Compare commits

...

68 commits

Author SHA1 Message Date
Sandeep Somavarapu 7dfcd74e63
#15756 adjust position of pre-release text 2021-11-26 20:09:01 +01:00
Sandeep Somavarapu fd83e2135c
#15756 show pre-release text in extension editor 2021-11-26 20:03:04 +01:00
Sandeep Somavarapu 438dc2f8b5
#15756 show pre-release indicator if extension is pre-release version 2021-11-26 20:03:04 +01:00
Benjamin Pasero df4b5d6d04
smoke - strengthen shutdown path 2021-11-26 18:08:27 +01:00
Henning Dieterichs e2adc711f3
Fixes lint error. 2021-11-26 17:39:25 +01:00
Henning Dieterichs 40c9c3f677
Disable highlighting ambiguous characters for plaintext. 2021-11-26 17:20:34 +01:00
Henning Dieterichs 319529dc9d
Disable highlighting ambiguous characters for markdown. 2021-11-26 17:20:34 +01:00
Sandeep Somavarapu 153a028f3b
rename 2021-11-26 15:08:33 +01:00
Sandeep Somavarapu 212deea1cc
#46851 passing extension description breaks intellisense as it is not serializable.
Pass only id and display name as extension info
2021-11-26 14:54:17 +01:00
Benjamin Pasero 13ff6baa3f
fix smoke tests 2021-11-26 14:45:35 +01:00
Sandeep Somavarapu 5cb1766018
#46851 pass extension description as source - adopt at other places 2021-11-26 14:09:12 +01:00
Sandeep Somavarapu 49fc9c109e
#46851 pass extension description as source 2021-11-26 14:02:48 +01:00
Sandeep Somavarapu 39c6132b2c
#134684 override default values from workbenchAssignmentsService for experimental settings 2021-11-26 13:35:13 +01:00
Johannes Rieken d673cdb0ec
some jsdoc for language status items, https://github.com/microsoft/vscode/issues/129037 2021-11-26 12:20:11 +01:00
Martin Aeschlimann c67fd6e5cd
Merge pull request #137557 from sijakret/html-language-server/virtual-doc-support
html-language-features: text document provider support for customData.html
2021-11-26 11:52:06 +01:00
Henning Dieterichs 22a1d0b1d2
Fixes presentation of code point. 2021-11-26 11:51:33 +01:00
Martin Aeschlimann f1455eabed
revert request changes & polish 2021-11-26 11:45:08 +01:00
Johannes Rieken 6b2aa3abfd
allow to mark a language status item as busy, https://github.com/microsoft/vscode/issues/129037 2021-11-26 11:42:02 +01:00
Henning Dieterichs bc75bda008
Merge pull request #137889 from microsoft/hediet/unicode-highlighting-banner
Shows a banner if a file has too many highlighted unicode characters.
2021-11-26 11:39:13 +01:00
Alex Dima a8b571c9f3
🆙 native-keymap 2021-11-26 11:28:53 +01:00
Benjamin Pasero 5ce5e6cc02
tests - really skip test (#137853) 2021-11-26 11:22:38 +01:00
Henning Dieterichs 8a305e17d5
Introduces interface to keep shortLabel alive. 2021-11-26 11:00:25 +01:00
Henning Dieterichs ab8b0b914a
Set height to 0 if domNode is null. 2021-11-26 11:00:25 +01:00
Henning Dieterichs 75d09de705
Moves reservedHeight computation into commonEditorConfig. 2021-11-26 11:00:24 +01:00
Henning Dieterichs 42ec6e7924
Shows a banner if a file has too many highlighted unicode characters. 2021-11-26 11:00:24 +01:00
Benjamin Pasero 8d250e99e7
smoke - wait for tab becoming dirty before exit 2021-11-26 11:00:15 +01:00
Sandeep Somavarapu bab15fcbb0
#15756 fine tuning wordings 2021-11-26 10:44:41 +01:00
Benjamin Pasero 420c749ca8
Rename resolveShellEnv to getResolvedShellEnv (fix #137923) 2021-11-26 10:28:49 +01:00
Sandeep Somavarapu bfd3bee273
#15756 prevent installing unsupported extensions in main service 2021-11-26 10:26:44 +01:00
Alex Dima 1a3dce1b0c
update monaco.d.ts 2021-11-26 10:13:08 +01:00
Alexandru Dima eca6f7ed24
Merge pull request #137810 from mkantor/doc-comment-typos
Fix a few typos in doc comments
2021-11-26 10:10:04 +01:00
Benjamin Pasero a11814c57a
smoke - switch away from potentially flaky editors.selectTab method 2021-11-26 09:22:19 +01:00
Benjamin Pasero 01d1ea52e6
tests - skip flaky in node.js env (#137853) 2021-11-26 08:56:58 +01:00
deepak1556 d18d093403 ci: remove xcode switch step
Refs 480888c7ce
2021-11-26 15:19:42 +09:00
deepak1556 480888c7ce ci: unset SDKROOT
macOS 11 is the default agent that has the correct headers
by default to build for arm target. The hack was needed when
the agent was still Catalina and the arm headers were not
available by default.
2021-11-26 13:19:47 +09:00
Sandeep Somavarapu a45c7f09cc
#15756
- show warning for unsupported extensions
- action to switch to prerelease
2021-11-26 02:02:41 +01:00
Sandeep Somavarapu 503a9bcd16
#15756 prompt users to migrate from old extension to main prerelease extension 2021-11-26 01:09:15 +01:00
Alexandru Dima 4aad18d229
Merge pull request #137863 from microsoft/alex/remote-connection-improvements
Remote connection improvements
2021-11-26 00:53:24 +01:00
Alex Dima 61eda668bd
Merge remote-tracking branch 'origin/main' into alex/remote-connection-improvements 2021-11-25 23:41:04 +01:00
Alex Dima 8dbd9d0ee6
Remove unnecessary socket.pause() calls 2021-11-25 23:40:30 +01:00
Martin Aeschlimann eecd0038f6
add previewColorTheme command (for #137289) 2021-11-25 22:51:41 +01:00
Alex Dima be87ebcd0d
Ask the client to pause writing until the socket is sent to the remote extension host process, which then asks the client to resume (#134429) 2021-11-25 22:41:43 +01:00
Martin Aeschlimann 1c1df5532d
themes actions: convert to Action2 2021-11-25 21:45:32 +01:00
Alex Dima 3fb9624b29
Remove KeepAlive message and rely solely on unacknowledged messages as a trigger for timeout 2021-11-25 21:07:14 +01:00
Martin Aeschlimann 118d34cbe7
add browse marketplace for file and product icons (for #137289) 2021-11-25 20:04:50 +01:00
Martin Aeschlimann 60d21965c7
add marketplace apis for file/product icon themes (for #137289) 2021-11-25 20:04:49 +01:00
Sandeep Somavarapu ab394ee788
#15756 support pre-release while installing through cli and commands 2021-11-25 19:55:21 +01:00
Sandeep Somavarapu dd19b1d50b
refactor extensions report to extensions control manifest 2021-11-25 19:16:06 +01:00
Alex Dima 7b3474abff
Make sure websocket frames are processed in order 2021-11-25 16:44:30 +01:00
Henning Dieterichs 10d3e93db5
Improves assertNever type. 2021-11-25 16:15:32 +01:00
Alex Dima a3dce400d6
Move draining logic to ZlibDeflateStream 2021-11-25 15:34:36 +01:00
Alex Dima 2b6fd1df46
Extract ZlibDeflateStream 2021-11-25 15:27:37 +01:00
Sandeep Somavarapu 944d343cc2
#46851 add default value source & default default value to configuration properties 2021-11-25 14:45:22 +01:00
Ladislau Szomoru f853123bff
Fix #137870 2021-11-25 14:37:35 +01:00
Benjamin Pasero 6f2239307b
Smoke test tweaks (#137809)
* smoke - move data migration tests into one and align

* fix app starting

* `createWorkspaceFile` is not async

* 💄

* support screenshot on failure even for stable app

* smoke - try to remove timeout (#137847)

* improve exit call
2021-11-25 14:37:22 +01:00
Martin Aeschlimann 2f8fb0b32e
workbench.colorCustomizations not working after the latest update. Fixes #137867 2021-11-25 13:53:42 +01:00
Alex Dima babe6a6a94
Extract ZlibInflateStream 2021-11-25 11:47:55 +01:00
Sandeep Somavarapu 5e5bb86a25
take extension id while registering defaults 2021-11-25 11:37:31 +01:00
Sandeep Somavarapu 131f9fa97c
#46851 fix init 2021-11-25 11:03:57 +01:00
Alex Dima 20d492a0a0
Merge remote-tracking branch 'origin/main' into alex/remote-connection-improvements 2021-11-25 10:26:09 +01:00
Alex Dima 228ac5b3c1
Add traceSocketEvent for fake socket 2021-11-25 10:25:00 +01:00
Martin Aeschlimann 7ca8fbe2d3
icon themes: fix duplicated marketplace item 2021-11-25 10:07:36 +01:00
Sandeep Somavarapu f04acdb07e
#15756 fine tune labels 2021-11-25 09:52:04 +01:00
Alex Dima 648e355c05
Add tracing to sockets 2021-11-25 09:40:16 +01:00
Jan Kretschmer bb89815cfb use regex, not Uri.parse, to detect custom scheme 2021-11-24 22:07:31 +01:00
Matt Kantor 602c83b7bc Fix a few typos in doc comments 2021-11-24 11:09:36 -05:00
Jan Kretschmer 8779aaf2ae use set to store and lookup paths of interest 2021-11-20 00:16:06 +01:00
Jan Kretschmer b074018c3e sketch for virtual document support for 2021-11-01 14:50:32 +01:00
103 changed files with 2629 additions and 1169 deletions

View file

@ -37,12 +37,6 @@ steps:
git config user.name "VSCode"
displayName: Prepare tooling
- script: |
set -e
sudo xcode-select -s /Applications/Xcode_12.2.app
displayName: Switch to Xcode 12
condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'arm64'))
- script: |
set -e
git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro")
@ -84,7 +78,6 @@ steps:
set -e
export npm_config_arch=$(VSCODE_ARCH)
export npm_config_node_gyp=$(which node-gyp)
export SDKROOT=/Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk
for i in {1..3}; do # try 3 times, for Terrapin
yarn --frozen-lockfile && break

View file

@ -6,6 +6,7 @@
import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode';
import { resolvePath, joinPath } from './requests';
export function getCustomDataSource(toDispose: Disposable[]) {
let pathsInWorkspace = getCustomDataPathsInAllWorkspaces();
let pathsInExtensions = getCustomDataPathsFromAllExtensions();
@ -14,7 +15,7 @@ export function getCustomDataSource(toDispose: Disposable[]) {
toDispose.push(extensions.onDidChange(_ => {
const newPathsInExtensions = getCustomDataPathsFromAllExtensions();
if (newPathsInExtensions.length !== pathsInExtensions.length || !newPathsInExtensions.every((val, idx) => val === pathsInExtensions[idx])) {
if (pathsInExtensions.size !== newPathsInExtensions.size || ![...pathsInExtensions].every(path => newPathsInExtensions.has(path))) {
pathsInExtensions = newPathsInExtensions;
onChange.fire();
}
@ -26,9 +27,16 @@ export function getCustomDataSource(toDispose: Disposable[]) {
}
}));
toDispose.push(workspace.onDidChangeTextDocument(e => {
const path = e.document.uri.toString();
if (pathsInExtensions.has(path) || pathsInWorkspace.has(path)) {
onChange.fire();
}
}));
return {
get uris() {
return pathsInWorkspace.concat(pathsInExtensions);
return [...pathsInWorkspace].concat([...pathsInExtensions]);
},
get onDidChange() {
return onChange.event;
@ -36,21 +44,31 @@ export function getCustomDataSource(toDispose: Disposable[]) {
};
}
function isURI(uriOrPath: string) {
return /^(?<scheme>\w[\w\d+.-]*):/.test(uriOrPath);
}
function getCustomDataPathsInAllWorkspaces(): string[] {
function getCustomDataPathsInAllWorkspaces(): Set<string> {
const workspaceFolders = workspace.workspaceFolders;
const dataPaths: string[] = [];
const dataPaths = new Set<string>();
if (!workspaceFolders) {
return dataPaths;
}
const collect = (paths: string[] | undefined, rootFolder: Uri) => {
if (Array.isArray(paths)) {
for (const path of paths) {
if (typeof path === 'string') {
dataPaths.push(resolvePath(rootFolder, path).toString());
const collect = (uriOrPaths: string[] | undefined, rootFolder: Uri) => {
if (Array.isArray(uriOrPaths)) {
for (const uriOrPath of uriOrPaths) {
if (typeof uriOrPath === 'string') {
if (!isURI(uriOrPath)) {
// path in the workspace
dataPaths.add(resolvePath(rootFolder, uriOrPath).toString());
} else {
// external uri
dataPaths.add(uriOrPath);
}
}
}
}
@ -74,13 +92,20 @@ function getCustomDataPathsInAllWorkspaces(): string[] {
return dataPaths;
}
function getCustomDataPathsFromAllExtensions(): string[] {
const dataPaths: string[] = [];
function getCustomDataPathsFromAllExtensions(): Set<string> {
const dataPaths = new Set<string>();
for (const extension of extensions.all) {
const customData = extension.packageJSON?.contributes?.html?.customData;
if (Array.isArray(customData)) {
for (const rp of customData) {
dataPaths.push(joinPath(extension.extensionUri, rp).toString());
for (const uriOrPath of customData) {
if (!isURI(uriOrPath)) {
// relative path in an extension
dataPaths.add(joinPath(extension.extensionUri, uriOrPath).toString());
} else {
// external uri
dataPaths.add(uriOrPath);
}
}
}
}

View file

@ -16,7 +16,7 @@ import {
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, NotificationType, CommonLanguageClient
} from 'vscode-languageclient';
import { activateTagClosing } from './tagClosing';
import { RequestService } from './requests';
import { RequestService, serveFileSystemRequests } from './requests';
import { getCustomDataSource } from './customData';
namespace CustomDataChangedNotification {
@ -120,6 +120,8 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
toDispose.push(disposable);
client.onReady().then(() => {
toDispose.push(serveFileSystemRequests(client, runtime));
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
customDataSource.onDidChange(() => {
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, workspace } from 'vscode';
import { Uri, workspace, Disposable } from 'vscode';
import { RequestType, CommonLanguageClient } from 'vscode-languageclient';
import { Runtime } from './htmlClient';
@ -18,8 +18,9 @@ export namespace FsReadDirRequest {
export const type: RequestType<string, [string, FileType][], any> = new RequestType('fs/readDir');
}
export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime) {
client.onRequest(FsContentRequest.type, (param: { uri: string; encoding?: string; }) => {
export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime): Disposable {
const disposables = [];
disposables.push(client.onRequest(FsContentRequest.type, (param: { uri: string; encoding?: string; }) => {
const uri = Uri.parse(param.uri);
if (uri.scheme === 'file' && runtime.fs) {
return runtime.fs.getContent(param.uri);
@ -27,21 +28,22 @@ export function serveFileSystemRequests(client: CommonLanguageClient, runtime: R
return workspace.fs.readFile(uri).then(buffer => {
return new runtime.TextDecoder(param.encoding).decode(buffer);
});
});
client.onRequest(FsReadDirRequest.type, (uriString: string) => {
}));
disposables.push(client.onRequest(FsReadDirRequest.type, (uriString: string) => {
const uri = Uri.parse(uriString);
if (uri.scheme === 'file' && runtime.fs) {
return runtime.fs.readDirectory(uriString);
}
return workspace.fs.readDirectory(uri);
});
client.onRequest(FsStatRequest.type, (uriString: string) => {
}));
disposables.push(client.onRequest(FsStatRequest.type, (uriString: string) => {
const uri = Uri.parse(uriString);
if (uri.scheme === 'file' && runtime.fs) {
return runtime.fs.stat(uriString);
}
return workspace.fs.stat(uri);
});
}));
return Disposable.from(...disposables);
}
export enum FileType {

View file

@ -87,7 +87,12 @@
"language": "markdown",
"path": "./snippets/markdown.code-snippets"
}
]
],
"configurationDefaults": {
"[markdown]": {
"editor.unicodeHighlight.ambiguousCharacters": false
}
}
},
"scripts": {
"update-grammar": "node ../node_modules/vscode-grammar-updater/bin microsoft/vscode-markdown-tm-grammar syntaxes/markdown.tmLanguage ./syntaxes/markdown.tmLanguage.json"

View file

@ -73,7 +73,7 @@
"keytar": "7.2.0",
"minimist": "^1.2.5",
"native-is-elevated": "0.4.3",
"native-keymap": "3.0.1",
"native-keymap": "3.0.2",
"native-watchdog": "1.3.0",
"node-pty": "0.11.0-beta11",
"spdlog": "^0.13.0",

View file

@ -43,6 +43,14 @@ export class VSBuffer {
}
}
static fromByteArray(source: number[]): VSBuffer {
const result = VSBuffer.alloc(source.length);
for (let i = 0, len = source.length; i < len; i++) {
result.buffer[i] = source[i];
}
return result;
}
static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer {
if (typeof totalLength === 'undefined') {
totalLength = 0;
@ -70,6 +78,12 @@ export class VSBuffer {
this.byteLength = this.buffer.byteLength;
}
clone(): VSBuffer {
const result = VSBuffer.alloc(this.byteLength);
result.set(this);
return result;
}
toString(): string {
if (hasBuffer) {
return this.buffer.toString();
@ -90,11 +104,20 @@ export class VSBuffer {
set(array: VSBuffer, offset?: number): void;
set(array: Uint8Array, offset?: number): void;
set(array: VSBuffer | Uint8Array, offset?: number): void {
set(array: ArrayBuffer, offset?: number): void;
set(array: ArrayBufferView, offset?: number): void;
set(array: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView, offset?: number): void;
set(array: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView, offset?: number): void {
if (array instanceof VSBuffer) {
this.buffer.set(array.buffer, offset);
} else {
} else if (array instanceof Uint8Array) {
this.buffer.set(array, offset);
} else if (array instanceof ArrayBuffer) {
this.buffer.set(new Uint8Array(array), offset);
} else if (ArrayBuffer.isView(array)) {
this.buffer.set(new Uint8Array(array.buffer, array.byteOffset, array.byteLength), offset);
} else {
throw new Error(`Unkown argument 'array'`);
}
}

View file

@ -11,6 +11,7 @@ let _isLinux = false;
let _isLinuxSnap = false;
let _isNative = false;
let _isWeb = false;
let _isElectron = false;
let _isIOS = false;
let _locale: string | undefined = undefined;
let _language: string = LANGUAGE_DEFAULT;
@ -61,7 +62,8 @@ if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== '
nodeProcess = process;
}
const isElectronRenderer = typeof nodeProcess?.versions?.electron === 'string' && nodeProcess.type === 'renderer';
const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string';
const isElectronRenderer = isElectronProcess && nodeProcess?.type === 'renderer';
export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
interface INavigator {
@ -89,6 +91,7 @@ else if (typeof nodeProcess === 'object') {
_isMacintosh = (nodeProcess.platform === 'darwin');
_isLinux = (nodeProcess.platform === 'linux');
_isLinuxSnap = _isLinux && !!nodeProcess.env['SNAP'] && !!nodeProcess.env['SNAP_REVISION'];
_isElectron = isElectronProcess;
_locale = LANGUAGE_DEFAULT;
_language = LANGUAGE_DEFAULT;
const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
@ -140,6 +143,7 @@ export const isMacintosh = _isMacintosh;
export const isLinux = _isLinux;
export const isLinuxSnap = _isLinuxSnap;
export const isNative = _isNative;
export const isElectron = _isElectron;
export const isWeb = _isWeb;
export const isIOS = _isIOS;
export const platform = _platform;

View file

@ -287,7 +287,7 @@ export function NotImplementedProxy<T>(name: string): { new(): T } {
};
}
export function assertNever(value: never, message = 'Unreachable') {
export function assertNever(value: never, message = 'Unreachable'): never {
throw new Error(message);
}

View file

@ -8,6 +8,89 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IIPCLogger, IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc';
export const enum SocketDiagnosticsEventType {
Created = 'created',
Read = 'read',
Write = 'write',
Open = 'open',
Error = 'error',
Close = 'close',
BrowserWebSocketBlobReceived = 'browserWebSocketBlobReceived',
NodeEndReceived = 'nodeEndReceived',
NodeEndSent = 'nodeEndSent',
NodeDrainBegin = 'nodeDrainBegin',
NodeDrainEnd = 'nodeDrainEnd',
zlibInflateError = 'zlibInflateError',
zlibInflateData = 'zlibInflateData',
zlibInflateInitialWrite = 'zlibInflateInitialWrite',
zlibInflateInitialFlushFired = 'zlibInflateInitialFlushFired',
zlibInflateWrite = 'zlibInflateWrite',
zlibInflateFlushFired = 'zlibInflateFlushFired',
zlibDeflateError = 'zlibDeflateError',
zlibDeflateData = 'zlibDeflateData',
zlibDeflateWrite = 'zlibDeflateWrite',
zlibDeflateFlushFired = 'zlibDeflateFlushFired',
WebSocketNodeSocketWrite = 'webSocketNodeSocketWrite',
WebSocketNodeSocketPeekedHeader = 'webSocketNodeSocketPeekedHeader',
WebSocketNodeSocketReadHeader = 'webSocketNodeSocketReadHeader',
WebSocketNodeSocketReadData = 'webSocketNodeSocketReadData',
WebSocketNodeSocketUnmaskedData = 'webSocketNodeSocketUnmaskedData',
WebSocketNodeSocketDrainBegin = 'webSocketNodeSocketDrainBegin',
WebSocketNodeSocketDrainEnd = 'webSocketNodeSocketDrainEnd',
ProtocolHeaderRead = 'protocolHeaderRead',
ProtocolMessageRead = 'protocolMessageRead',
ProtocolHeaderWrite = 'protocolHeaderWrite',
ProtocolMessageWrite = 'protocolMessageWrite',
ProtocolWrite = 'protocolWrite',
}
export namespace SocketDiagnostics {
export const enableDiagnostics = false;
export interface IRecord {
timestamp: number;
id: string;
label: string;
type: SocketDiagnosticsEventType;
buff?: VSBuffer;
data?: any;
}
export const records: IRecord[] = [];
const socketIds = new WeakMap<any, string>();
let lastUsedSocketId = 0;
function getSocketId(nativeObject: any, label: string): string {
if (!socketIds.has(nativeObject)) {
const id = String(++lastUsedSocketId);
socketIds.set(nativeObject, id);
}
return socketIds.get(nativeObject)!;
}
export function traceSocketEvent(nativeObject: any, socketDebugLabel: string, type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void {
if (!enableDiagnostics) {
return;
}
const id = getSocketId(nativeObject, socketDebugLabel);
if (data instanceof VSBuffer || data instanceof Uint8Array || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const copiedData = VSBuffer.alloc(data.byteLength);
copiedData.set(data);
records.push({ timestamp: Date.now(), id, label: socketDebugLabel, type, buff: copiedData });
} else {
// data is a custom object
records.push({ timestamp: Date.now(), id, label: socketDebugLabel, type, data: data });
}
}
}
export const enum SocketCloseEventType {
NodeSocketCloseEvent = 0,
WebSocketCloseEvent = 1
@ -60,6 +143,8 @@ export interface ISocket extends IDisposable {
write(buffer: VSBuffer): void;
end(): void;
drain(): Promise<void>;
traceSocketEvent(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void;
}
let emptyBuffer: VSBuffer | null = null;
@ -168,9 +253,23 @@ const enum ProtocolMessageType {
Regular = 1,
Control = 2,
Ack = 3,
KeepAlive = 4,
Disconnect = 5,
ReplayRequest = 6
ReplayRequest = 6,
Pause = 7,
Resume = 8
}
function protocolMessageTypeToString(messageType: ProtocolMessageType) {
switch (messageType) {
case ProtocolMessageType.None: return 'None';
case ProtocolMessageType.Regular: return 'Regular';
case ProtocolMessageType.Control: return 'Control';
case ProtocolMessageType.Ack: return 'Ack';
case ProtocolMessageType.Disconnect: return 'Disconnect';
case ProtocolMessageType.ReplayRequest: return 'ReplayRequest';
case ProtocolMessageType.Pause: return 'PauseWriting';
case ProtocolMessageType.Resume: return 'ResumeWriting';
}
}
export const enum ProtocolConstants {
@ -180,17 +279,11 @@ export const enum ProtocolConstants {
*/
AcknowledgeTime = 2000, // 2 seconds
/**
* If there is a message that has been unacknowledged for 10 seconds, consider the connection closed...
* If there is a sent message that has been unacknowledged for 20 seconds,
* and we didn't see any incoming server data in the past 20 seconds,
* then consider the connection has timed out.
*/
AcknowledgeTimeoutTime = 20000, // 20 seconds
/**
* Send at least a message every 5s for keep alive reasons.
*/
KeepAliveTime = 5000, // 5 seconds
/**
* If there is no message received for 10 seconds, consider the connection closed...
*/
KeepAliveTimeoutTime = 20000, // 20 seconds
TimeoutTime = 20000, // 20 seconds
/**
* If there is no reconnection within this time-frame, consider the connection permanently closed...
*/
@ -268,6 +361,9 @@ class ProtocolReader extends Disposable {
this._state.messageType = buff.readUInt8(0);
this._state.id = buff.readUInt32BE(1);
this._state.ack = buff.readUInt32BE(5);
this._socket.traceSocketEvent(SocketDiagnosticsEventType.ProtocolHeaderRead, { messageType: protocolMessageTypeToString(this._state.messageType), id: this._state.id, ack: this._state.ack, messageSize: this._state.readLen });
} else {
// buff is the body
const messageType = this._state.messageType;
@ -281,6 +377,8 @@ class ProtocolReader extends Disposable {
this._state.id = 0;
this._state.ack = 0;
this._socket.traceSocketEvent(SocketDiagnosticsEventType.ProtocolMessageRead, buff);
this._onMessage.fire(new ProtocolMessage(messageType, id, ack, buff));
if (this._isDisposed) {
@ -304,6 +402,7 @@ class ProtocolReader extends Disposable {
class ProtocolWriter {
private _isDisposed: boolean;
private _isPaused: boolean;
private readonly _socket: ISocket;
private _data: VSBuffer[];
private _totalLength: number;
@ -311,6 +410,7 @@ class ProtocolWriter {
constructor(socket: ISocket) {
this._isDisposed = false;
this._isPaused = false;
this._socket = socket;
this._data = [];
this._totalLength = 0;
@ -336,6 +436,15 @@ class ProtocolWriter {
this._writeNow();
}
public pause(): void {
this._isPaused = true;
}
public resume(): void {
this._isPaused = false;
this._scheduleWriting();
}
public write(msg: ProtocolMessage) {
if (this._isDisposed) {
// ignore: there could be left-over promises which complete and then
@ -349,6 +458,10 @@ class ProtocolWriter {
header.writeUInt32BE(msg.id, 1);
header.writeUInt32BE(msg.ack, 5);
header.writeUInt32BE(msg.data.byteLength, 9);
this._socket.traceSocketEvent(SocketDiagnosticsEventType.ProtocolHeaderWrite, { messageType: protocolMessageTypeToString(msg.type), id: msg.id, ack: msg.ack, messageSize: msg.data.byteLength });
this._socket.traceSocketEvent(SocketDiagnosticsEventType.ProtocolMessageWrite, msg.data);
this._writeSoon(header, msg.data);
}
@ -368,17 +481,31 @@ class ProtocolWriter {
private _writeSoon(header: VSBuffer, data: VSBuffer): void {
if (this._bufferAdd(header, data)) {
setTimeout(() => {
this._writeNow();
});
this._scheduleWriting();
}
}
private _writeNowTimeout: any = null;
private _scheduleWriting(): void {
if (this._writeNowTimeout) {
return;
}
this._writeNowTimeout = setTimeout(() => {
this._writeNowTimeout = null;
this._writeNow();
});
}
private _writeNow(): void {
if (this._totalLength === 0) {
return;
}
this._socket.write(this._bufferTake());
if (this._isPaused) {
return;
}
const data = this._bufferTake();
this._socket.traceSocketEvent(SocketDiagnosticsEventType.ProtocolWrite, { byteLength: data.byteLength });
this._socket.write(data);
}
}
@ -650,9 +777,6 @@ export class PersistentProtocol implements IMessagePassingProtocol {
private _incomingMsgLastTime: number;
private _incomingAckTimeout: any | null;
private _outgoingKeepAliveTimeout: any | null;
private _incomingKeepAliveTimeout: any | null;
private _lastReplayRequestTime: number;
private _socket: ISocket;
@ -694,9 +818,6 @@ export class PersistentProtocol implements IMessagePassingProtocol {
this._incomingMsgLastTime = 0;
this._incomingAckTimeout = null;
this._outgoingKeepAliveTimeout = null;
this._incomingKeepAliveTimeout = null;
this._lastReplayRequestTime = 0;
this._socketDisposables = [];
@ -710,9 +831,6 @@ export class PersistentProtocol implements IMessagePassingProtocol {
if (initialChunk) {
this._socketReader.acceptChunk(initialChunk);
}
this._sendKeepAliveCheck();
this._recvKeepAliveCheck();
}
dispose(): void {
@ -724,14 +842,6 @@ export class PersistentProtocol implements IMessagePassingProtocol {
clearTimeout(this._incomingAckTimeout);
this._incomingAckTimeout = null;
}
if (this._outgoingKeepAliveTimeout) {
clearTimeout(this._outgoingKeepAliveTimeout);
this._outgoingKeepAliveTimeout = null;
}
if (this._incomingKeepAliveTimeout) {
clearTimeout(this._incomingKeepAliveTimeout);
this._incomingKeepAliveTimeout = null;
}
this._socketDisposables = dispose(this._socketDisposables);
}
@ -745,50 +855,18 @@ export class PersistentProtocol implements IMessagePassingProtocol {
this._socketWriter.flush();
}
private _sendKeepAliveCheck(): void {
if (this._outgoingKeepAliveTimeout) {
// there will be a check in the near future
return;
}
const timeSinceLastOutgoingMsg = Date.now() - this._socketWriter.lastWriteTime;
if (timeSinceLastOutgoingMsg >= ProtocolConstants.KeepAliveTime) {
// sufficient time has passed since last message was written,
// and no message from our side needed to be sent in the meantime,
// so we will send a message containing only a keep alive.
const msg = new ProtocolMessage(ProtocolMessageType.KeepAlive, 0, 0, getEmptyBuffer());
this._socketWriter.write(msg);
this._sendKeepAliveCheck();
return;
}
this._outgoingKeepAliveTimeout = setTimeout(() => {
this._outgoingKeepAliveTimeout = null;
this._sendKeepAliveCheck();
}, ProtocolConstants.KeepAliveTime - timeSinceLastOutgoingMsg + 5);
sendPause(): void {
const msg = new ProtocolMessage(ProtocolMessageType.Pause, 0, 0, getEmptyBuffer());
this._socketWriter.write(msg);
}
private _recvKeepAliveCheck(): void {
if (this._incomingKeepAliveTimeout) {
// there will be a check in the near future
return;
}
sendResume(): void {
const msg = new ProtocolMessage(ProtocolMessageType.Resume, 0, 0, getEmptyBuffer());
this._socketWriter.write(msg);
}
const timeSinceLastIncomingMsg = Date.now() - this._socketReader.lastReadTime;
if (timeSinceLastIncomingMsg >= ProtocolConstants.KeepAliveTimeoutTime) {
// It's been a long time since we received a server message
// But this might be caused by the event loop being busy and failing to read messages
if (!this._loadEstimator.hasHighLoad()) {
// Trash the socket
this._onSocketTimeout.fire(undefined);
return;
}
}
this._incomingKeepAliveTimeout = setTimeout(() => {
this._incomingKeepAliveTimeout = null;
this._recvKeepAliveCheck();
}, Math.max(ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg, 0) + 5);
pauseSocketWriting() {
this._socketWriter.pause();
}
public getSocket(): ISocket {
@ -829,9 +907,6 @@ export class PersistentProtocol implements IMessagePassingProtocol {
this._socketWriter.write(toSend[i]);
}
this._recvAckCheck();
this._sendKeepAliveCheck();
this._recvKeepAliveCheck();
}
public acceptDisconnect(): void {
@ -852,34 +927,59 @@ export class PersistentProtocol implements IMessagePassingProtocol {
} while (true);
}
if (msg.type === ProtocolMessageType.Regular) {
if (msg.id > this._incomingMsgId) {
if (msg.id !== this._incomingMsgId + 1) {
// in case we missed some messages we ask the other party to resend them
const now = Date.now();
if (now - this._lastReplayRequestTime > 10000) {
// send a replay request at most once every 10s
this._lastReplayRequestTime = now;
this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.ReplayRequest, 0, 0, getEmptyBuffer()));
switch (msg.type) {
case ProtocolMessageType.None: {
// N/A
break;
}
case ProtocolMessageType.Regular: {
if (msg.id > this._incomingMsgId) {
if (msg.id !== this._incomingMsgId + 1) {
// in case we missed some messages we ask the other party to resend them
const now = Date.now();
if (now - this._lastReplayRequestTime > 10000) {
// send a replay request at most once every 10s
this._lastReplayRequestTime = now;
this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.ReplayRequest, 0, 0, getEmptyBuffer()));
}
} else {
this._incomingMsgId = msg.id;
this._incomingMsgLastTime = Date.now();
this._sendAckCheck();
this._onMessage.fire(msg.data);
}
} else {
this._incomingMsgId = msg.id;
this._incomingMsgLastTime = Date.now();
this._sendAckCheck();
this._onMessage.fire(msg.data);
}
break;
}
} else if (msg.type === ProtocolMessageType.Control) {
this._onControlMessage.fire(msg.data);
} else if (msg.type === ProtocolMessageType.Disconnect) {
this._onDidDispose.fire();
} else if (msg.type === ProtocolMessageType.ReplayRequest) {
// Send again all unacknowledged messages
const toSend = this._outgoingUnackMsg.toArray();
for (let i = 0, len = toSend.length; i < len; i++) {
this._socketWriter.write(toSend[i]);
case ProtocolMessageType.Control: {
this._onControlMessage.fire(msg.data);
break;
}
case ProtocolMessageType.Ack: {
// nothing to do
break;
}
case ProtocolMessageType.Disconnect: {
this._onDidDispose.fire();
break;
}
case ProtocolMessageType.ReplayRequest: {
// Send again all unacknowledged messages
const toSend = this._outgoingUnackMsg.toArray();
for (let i = 0, len = toSend.length; i < len; i++) {
this._socketWriter.write(toSend[i]);
}
this._recvAckCheck();
break;
}
case ProtocolMessageType.Pause: {
this._socketWriter.pause();
break;
}
case ProtocolMessageType.Resume: {
this._socketWriter.resume();
break;
}
this._recvAckCheck();
}
}
@ -956,8 +1056,15 @@ export class PersistentProtocol implements IMessagePassingProtocol {
const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek()!;
const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime;
if (timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.AcknowledgeTimeoutTime) {
const timeSinceLastReceivedSomeData = Date.now() - this._socketReader.lastReadTime;
if (
timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.TimeoutTime
&& timeSinceLastReceivedSomeData >= ProtocolConstants.TimeoutTime
) {
// It's been a long time since our sent message was acknowledged
// and a long time since we received some data
// But this might be caused by the event loop being busy and failing to read messages
if (!this._loadEstimator.hasHighLoad()) {
// Trash the socket
@ -969,7 +1076,7 @@ export class PersistentProtocol implements IMessagePassingProtocol {
this._outgoingAckTimeout = setTimeout(() => {
this._outgoingAckTimeout = null;
this._recvAckCheck();
}, Math.max(ProtocolConstants.AcknowledgeTimeoutTime - timeSinceOldestUnacknowledgedMsg, 0) + 5);
}, Math.max(ProtocolConstants.TimeoutTime - timeSinceOldestUnacknowledgedMsg, 500));
}
private _sendAck(): void {
@ -983,3 +1090,39 @@ export class PersistentProtocol implements IMessagePassingProtocol {
this._socketWriter.write(msg);
}
}
// (() => {
// if (!SocketDiagnostics.enableDiagnostics) {
// return;
// }
// if (typeof require.__$__nodeRequire !== 'function') {
// console.log(`Can only log socket diagnostics on native platforms.`);
// return;
// }
// const type = (
// process.argv.includes('--type=renderer')
// ? 'renderer'
// : (process.argv.includes('--type=extensionHost')
// ? 'extensionHost'
// : (process.argv.some(item => item.includes('server/main'))
// ? 'server'
// : 'unknown'
// )
// )
// );
// setTimeout(() => {
// SocketDiagnostics.records.forEach(r => {
// if (r.buff) {
// r.data = Buffer.from(r.buff.buffer).toString('base64');
// r.buff = undefined;
// }
// });
// const fs = <typeof import('fs')>require.__$__nodeRequire('fs');
// const path = <typeof import('path')>require.__$__nodeRequire('path');
// const logPath = path.join(process.cwd(),`${type}-${process.pid}`);
// console.log(`dumping socket diagnostics at ${logPath}`);
// fs.writeFileSync(logPath, JSON.stringify(SocketDiagnostics.records));
// }, 20000);
// })();

View file

@ -14,17 +14,25 @@ import { join } from 'vs/base/common/path';
import { Platform, platform } from 'vs/base/common/platform';
import { generateUuid } from 'vs/base/common/uuid';
import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc';
import { ChunkStream, Client, ISocket, Protocol, SocketCloseEvent, SocketCloseEventType } from 'vs/base/parts/ipc/common/ipc.net';
import { ChunkStream, Client, ISocket, Protocol, SocketCloseEvent, SocketCloseEventType, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net';
import * as zlib from 'zlib';
export class NodeSocket implements ISocket {
public readonly debugLabel: string;
public readonly socket: Socket;
private readonly _errorListener: (err: any) => void;
constructor(socket: Socket) {
public traceSocketEvent(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void {
SocketDiagnostics.traceSocketEvent(this.socket, this.debugLabel, type, data);
}
constructor(socket: Socket, debugLabel: string = '') {
this.debugLabel = debugLabel;
this.socket = socket;
this.traceSocketEvent(SocketDiagnosticsEventType.Created, { type: 'NodeSocket' });
this._errorListener = (err: any) => {
this.traceSocketEvent(SocketDiagnosticsEventType.Error, { code: err?.code, message: err?.message });
if (err) {
if (err.code === 'EPIPE') {
// An EPIPE exception at the wrong time can lead to a renderer process crash
@ -47,7 +55,10 @@ export class NodeSocket implements ISocket {
}
public onData(_listener: (e: VSBuffer) => void): IDisposable {
const listener = (buff: Buffer) => _listener(VSBuffer.wrap(buff));
const listener = (buff: Buffer) => {
this.traceSocketEvent(SocketDiagnosticsEventType.Read, buff);
_listener(VSBuffer.wrap(buff));
};
this.socket.on('data', listener);
return {
dispose: () => this.socket.off('data', listener)
@ -56,6 +67,7 @@ export class NodeSocket implements ISocket {
public onClose(listener: (e: SocketCloseEvent) => void): IDisposable {
const adapter = (hadError: boolean) => {
this.traceSocketEvent(SocketDiagnosticsEventType.Close, { hadError });
listener({
type: SocketCloseEventType.NodeSocketCloseEvent,
hadError: hadError,
@ -69,9 +81,13 @@ export class NodeSocket implements ISocket {
}
public onEnd(listener: () => void): IDisposable {
this.socket.on('end', listener);
const adapter = () => {
this.traceSocketEvent(SocketDiagnosticsEventType.NodeEndReceived);
listener();
};
this.socket.on('end', adapter);
return {
dispose: () => this.socket.off('end', listener)
dispose: () => this.socket.off('end', adapter)
};
}
@ -87,7 +103,8 @@ export class NodeSocket implements ISocket {
// > However, the false return value is only advisory and the writable stream will unconditionally
// > accept and buffer chunk even if it has not been allowed to drain.
try {
this.socket.write(<Buffer>buffer.buffer, (err: any) => {
this.traceSocketEvent(SocketDiagnosticsEventType.Write, buffer);
this.socket.write(buffer.buffer, (err: any) => {
if (err) {
if (err.code === 'EPIPE') {
// An EPIPE exception at the wrong time can lead to a renderer process crash
@ -116,12 +133,15 @@ export class NodeSocket implements ISocket {
}
public end(): void {
this.traceSocketEvent(SocketDiagnosticsEventType.NodeEndSent);
this.socket.end();
}
public drain(): Promise<void> {
this.traceSocketEvent(SocketDiagnosticsEventType.NodeDrainBegin);
return new Promise<void>((resolve, reject) => {
if (this.socket.bufferSize === 0) {
this.traceSocketEvent(SocketDiagnosticsEventType.NodeDrainEnd);
resolve();
return;
}
@ -131,6 +151,7 @@ export class NodeSocket implements ISocket {
this.socket.off('error', finished);
this.socket.off('timeout', finished);
this.socket.off('drain', finished);
this.traceSocketEvent(SocketDiagnosticsEventType.NodeDrainEnd);
resolve();
};
this.socket.on('close', finished);
@ -153,25 +174,17 @@ const enum ReadState {
Fin = 4
}
interface ISocketTracer {
traceSocketEvent(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void
}
/**
* See https://tools.ietf.org/html/rfc6455#section-5.2
*/
export class WebSocketNodeSocket extends Disposable implements ISocket {
export class WebSocketNodeSocket extends Disposable implements ISocket, ISocketTracer {
public readonly socket: NodeSocket;
public readonly permessageDeflate: boolean;
private _totalIncomingWireBytes: number;
private _totalIncomingDataBytes: number;
private _totalOutgoingWireBytes: number;
private _totalOutgoingDataBytes: number;
private readonly _zlibInflate: zlib.InflateRaw | null;
private readonly _zlibDeflate: zlib.DeflateRaw | null;
private _zlibDeflateFlushWaitingCount: number;
private readonly _onDidZlibFlush = this._register(new Emitter<void>());
private readonly _recordInflateBytes: boolean;
private readonly _recordedInflateBytes: Buffer[] = [];
private readonly _pendingInflateData: Buffer[] = [];
private readonly _pendingDeflateData: Buffer[] = [];
private readonly _flowManager: WebSocketFlowManager;
private readonly _incomingData: ChunkStream;
private readonly _onData = this._register(new Emitter<VSBuffer>());
private readonly _onClose = this._register(new Emitter<SocketCloseEvent>());
@ -186,27 +199,16 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
mask: 0
};
public get totalIncomingWireBytes(): number {
return this._totalIncomingWireBytes;
}
public get totalIncomingDataBytes(): number {
return this._totalIncomingDataBytes;
}
public get totalOutgoingWireBytes(): number {
return this._totalOutgoingWireBytes;
}
public get totalOutgoingDataBytes(): number {
return this._totalOutgoingDataBytes;
public get permessageDeflate(): boolean {
return this._flowManager.permessageDeflate;
}
public get recordedInflateBytes(): VSBuffer {
if (this._recordInflateBytes) {
return VSBuffer.wrap(Buffer.concat(this._recordedInflateBytes));
}
return VSBuffer.alloc(0);
return this._flowManager.recordedInflateBytes;
}
public traceSocketEvent(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void {
this.socket.traceSocketEvent(type, data);
}
/**
@ -224,69 +226,34 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
constructor(socket: NodeSocket, permessageDeflate: boolean, inflateBytes: VSBuffer | null, recordInflateBytes: boolean) {
super();
this.socket = socket;
this._totalIncomingWireBytes = 0;
this._totalIncomingDataBytes = 0;
this._totalOutgoingWireBytes = 0;
this._totalOutgoingDataBytes = 0;
this.permessageDeflate = permessageDeflate;
this._recordInflateBytes = recordInflateBytes;
if (permessageDeflate) {
// See https://tools.ietf.org/html/rfc7692#page-16
// To simplify our logic, we don't negotiate the window size
// and simply dedicate (2^15) / 32kb per web socket
this._zlibInflate = zlib.createInflateRaw({
windowBits: 15
this.traceSocketEvent(SocketDiagnosticsEventType.Created, { type: 'WebSocketNodeSocket', permessageDeflate, inflateBytesLength: inflateBytes?.byteLength || 0, recordInflateBytes });
this._flowManager = this._register(new WebSocketFlowManager(
this,
permessageDeflate,
inflateBytes,
recordInflateBytes,
this._onData,
(data, compressed) => this._write(data, compressed)
));
this._register(this._flowManager.onError((err) => {
// zlib errors are fatal, since we have no idea how to recover
console.error(err);
onUnexpectedError(err);
this._onClose.fire({
type: SocketCloseEventType.NodeSocketCloseEvent,
hadError: true,
error: err
});
this._zlibInflate.on('error', (err) => {
// zlib errors are fatal, since we have no idea how to recover
console.error(err);
onUnexpectedError(err);
this._onClose.fire({
type: SocketCloseEventType.NodeSocketCloseEvent,
hadError: true,
error: err
});
});
this._zlibInflate.on('data', (data: Buffer) => {
this._pendingInflateData.push(data);
});
if (inflateBytes) {
this._zlibInflate.write(inflateBytes.buffer);
this._zlibInflate.flush(() => {
this._pendingInflateData.length = 0;
});
}
this._zlibDeflate = zlib.createDeflateRaw({
windowBits: 15
});
this._zlibDeflate.on('error', (err) => {
// zlib errors are fatal, since we have no idea how to recover
console.error(err);
onUnexpectedError(err);
this._onClose.fire({
type: SocketCloseEventType.NodeSocketCloseEvent,
hadError: true,
error: err
});
});
this._zlibDeflate.on('data', (data: Buffer) => {
this._pendingDeflateData.push(data);
});
} else {
this._zlibInflate = null;
this._zlibDeflate = null;
}
this._zlibDeflateFlushWaitingCount = 0;
}));
this._incomingData = new ChunkStream();
this._register(this.socket.onData(data => this._acceptChunk(data)));
this._register(this.socket.onClose((e) => this._onClose.fire(e)));
}
public override dispose(): void {
if (this._zlibDeflateFlushWaitingCount > 0) {
if (this._flowManager.isProcessingWriteQueue()) {
// Wait for any outstanding writes to finish before disposing
this._register(this._onDidZlibFlush.event(() => {
this._register(this._flowManager.onDidFinishProcessingWriteQueue(() => {
this.dispose();
}));
} else {
@ -308,36 +275,16 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
}
public write(buffer: VSBuffer): void {
this._totalOutgoingDataBytes += buffer.byteLength;
if (this._zlibDeflate) {
this._zlibDeflate.write(<Buffer>buffer.buffer);
this._zlibDeflateFlushWaitingCount++;
// See https://zlib.net/manual.html#Constants
this._zlibDeflate.flush(/*Z_SYNC_FLUSH*/2, () => {
this._zlibDeflateFlushWaitingCount--;
let data = Buffer.concat(this._pendingDeflateData);
this._pendingDeflateData.length = 0;
// See https://tools.ietf.org/html/rfc7692#section-7.2.1
data = data.slice(0, data.length - 4);
if (!this._isEnded) {
// Avoid ERR_STREAM_WRITE_AFTER_END
this._write(VSBuffer.wrap(data), true);
}
if (this._zlibDeflateFlushWaitingCount === 0) {
this._onDidZlibFlush.fire();
}
});
} else {
this._write(buffer, false);
}
this._flowManager.writeMessage(buffer);
}
private _write(buffer: VSBuffer, compressed: boolean): void {
if (this._isEnded) {
// Avoid ERR_STREAM_WRITE_AFTER_END
return;
}
this.traceSocketEvent(SocketDiagnosticsEventType.WebSocketNodeSocketWrite, buffer);
let headerLen = Constants.MinHeaderByteSize;
if (buffer.byteLength < 126) {
headerLen += 0;
@ -374,7 +321,6 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
}
this._totalOutgoingWireBytes += header.byteLength + buffer.byteLength;
this.socket.write(VSBuffer.concat([header, buffer]));
}
@ -387,7 +333,6 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
if (data.byteLength === 0) {
return;
}
this._totalIncomingWireBytes += data.byteLength;
this._incomingData.acceptChunk(data);
@ -413,6 +358,8 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
this._state.firstFrameOfMessage = Boolean(finBit);
this._state.mask = 0;
this.traceSocketEvent(SocketDiagnosticsEventType.WebSocketNodeSocketPeekedHeader, { headerSize: this._state.readLen, compressed: this._state.compressed, fin: this._state.fin });
} else if (this._state.state === ReadState.ReadHeader) {
// read entire header
const header = this._incomingData.read(this._state.readLen);
@ -453,52 +400,268 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
this._state.readLen = len;
this._state.mask = mask;
this.traceSocketEvent(SocketDiagnosticsEventType.WebSocketNodeSocketPeekedHeader, { bodySize: this._state.readLen, compressed: this._state.compressed, fin: this._state.fin, mask: this._state.mask });
} else if (this._state.state === ReadState.ReadBody) {
// read body
const body = this._incomingData.read(this._state.readLen);
this.traceSocketEvent(SocketDiagnosticsEventType.WebSocketNodeSocketReadData, body);
unmask(body, this._state.mask);
this.traceSocketEvent(SocketDiagnosticsEventType.WebSocketNodeSocketUnmaskedData, body);
this._state.state = ReadState.PeekHeader;
this._state.readLen = Constants.MinHeaderByteSize;
this._state.mask = 0;
if (this._zlibInflate && this._state.compressed) {
// See https://datatracker.ietf.org/doc/html/rfc7692#section-9.2
// Even if permessageDeflate is negotiated, it is possible
// that the other side might decide to send uncompressed messages
// So only decompress messages that have the RSV 1 bit set
//
// See https://tools.ietf.org/html/rfc7692#section-7.2.2
if (this._recordInflateBytes) {
this._recordedInflateBytes.push(Buffer.from(<Buffer>body.buffer));
}
this._zlibInflate.write(<Buffer>body.buffer);
if (this._state.fin) {
if (this._recordInflateBytes) {
this._recordedInflateBytes.push(Buffer.from([0x00, 0x00, 0xff, 0xff]));
}
this._zlibInflate.write(Buffer.from([0x00, 0x00, 0xff, 0xff]));
}
this._zlibInflate.flush(() => {
const data = Buffer.concat(this._pendingInflateData);
this._pendingInflateData.length = 0;
this._totalIncomingDataBytes += data.length;
this._onData.fire(VSBuffer.wrap(data));
});
} else {
this._totalIncomingDataBytes += body.byteLength;
this._onData.fire(body);
}
this._flowManager.acceptFrame(body, this._state.compressed, !!this._state.fin);
}
}
}
public async drain(): Promise<void> {
if (this._zlibDeflateFlushWaitingCount > 0) {
await Event.toPromise(this._onDidZlibFlush.event);
this.traceSocketEvent(SocketDiagnosticsEventType.WebSocketNodeSocketDrainBegin);
if (this._flowManager.isProcessingWriteQueue()) {
await Event.toPromise(this._flowManager.onDidFinishProcessingWriteQueue);
}
await this.socket.drain();
this.traceSocketEvent(SocketDiagnosticsEventType.WebSocketNodeSocketDrainEnd);
}
}
class WebSocketFlowManager extends Disposable {
private readonly _onError = this._register(new Emitter<Error>());
public readonly onError = this._onError.event;
private readonly _zlibInflateStream: ZlibInflateStream | null;
private readonly _zlibDeflateStream: ZlibDeflateStream | null;
private readonly _writeQueue: VSBuffer[] = [];
private readonly _readQueue: { data: VSBuffer, isCompressed: boolean, isLastFrameOfMessage: boolean }[] = [];
private readonly _onDidFinishProcessingWriteQueue = this._register(new Emitter<void>());
public readonly onDidFinishProcessingWriteQueue = this._onDidFinishProcessingWriteQueue.event;
public get permessageDeflate(): boolean {
return Boolean(this._zlibInflateStream && this._zlibDeflateStream);
}
public get recordedInflateBytes(): VSBuffer {
if (this._zlibInflateStream) {
return this._zlibInflateStream.recordedInflateBytes;
}
return VSBuffer.alloc(0);
}
constructor(
private readonly _tracer: ISocketTracer,
permessageDeflate: boolean,
inflateBytes: VSBuffer | null,
recordInflateBytes: boolean,
private readonly _onData: Emitter<VSBuffer>,
private readonly _writeFn: (data: VSBuffer, compressed: boolean) => void
) {
super();
if (permessageDeflate) {
// See https://tools.ietf.org/html/rfc7692#page-16
// To simplify our logic, we don't negotiate the window size
// and simply dedicate (2^15) / 32kb per web socket
this._zlibInflateStream = this._register(new ZlibInflateStream(this._tracer, recordInflateBytes, inflateBytes, { windowBits: 15 }));
this._zlibDeflateStream = this._register(new ZlibDeflateStream(this._tracer, { windowBits: 15 }));
this._register(this._zlibInflateStream.onError((err) => this._onError.fire(err)));
this._register(this._zlibDeflateStream.onError((err) => this._onError.fire(err)));
} else {
this._zlibInflateStream = null;
this._zlibDeflateStream = null;
}
}
public writeMessage(message: VSBuffer): void {
this._writeQueue.push(message);
this._processWriteQueue();
}
private _isProcessingWriteQueue = false;
private async _processWriteQueue(): Promise<void> {
if (this._isProcessingWriteQueue) {
return;
}
this._isProcessingWriteQueue = true;
while (this._writeQueue.length > 0) {
const message = this._writeQueue.shift()!;
if (this._zlibDeflateStream) {
const data = await this._deflateMessage(this._zlibDeflateStream, message);
this._writeFn(data, true);
} else {
this._writeFn(message, false);
}
}
this._isProcessingWriteQueue = false;
this._onDidFinishProcessingWriteQueue.fire();
}
public isProcessingWriteQueue(): boolean {
return (this._isProcessingWriteQueue);
}
/**
* Subsequent calls should wait for the previous `_deflateBuffer` call to complete.
*/
private _deflateMessage(zlibDeflateStream: ZlibDeflateStream, buffer: VSBuffer): Promise<VSBuffer> {
return new Promise<VSBuffer>((resolve, reject) => {
zlibDeflateStream.write(buffer);
zlibDeflateStream.flush(data => resolve(data));
});
}
public acceptFrame(data: VSBuffer, isCompressed: boolean, isLastFrameOfMessage: boolean): void {
this._readQueue.push({ data, isCompressed, isLastFrameOfMessage });
this._processReadQueue();
}
private _isProcessingReadQueue = false;
private async _processReadQueue(): Promise<void> {
if (this._isProcessingReadQueue) {
return;
}
this._isProcessingReadQueue = true;
while (this._readQueue.length > 0) {
const frameInfo = this._readQueue.shift()!;
if (this._zlibInflateStream && frameInfo.isCompressed) {
// See https://datatracker.ietf.org/doc/html/rfc7692#section-9.2
// Even if permessageDeflate is negotiated, it is possible
// that the other side might decide to send uncompressed messages
// So only decompress messages that have the RSV 1 bit set
const data = await this._inflateFrame(this._zlibInflateStream, frameInfo.data, frameInfo.isLastFrameOfMessage);
this._onData.fire(data);
} else {
this._onData.fire(frameInfo.data);
}
}
this._isProcessingReadQueue = false;
}
/**
* Subsequent calls should wait for the previous `transformRead` call to complete.
*/
private _inflateFrame(zlibInflateStream: ZlibInflateStream, buffer: VSBuffer, isLastFrameOfMessage: boolean): Promise<VSBuffer> {
return new Promise<VSBuffer>((resolve, reject) => {
// See https://tools.ietf.org/html/rfc7692#section-7.2.2
zlibInflateStream.write(buffer);
if (isLastFrameOfMessage) {
zlibInflateStream.write(VSBuffer.fromByteArray([0x00, 0x00, 0xff, 0xff]));
}
zlibInflateStream.flush(data => resolve(data));
});
}
}
class ZlibInflateStream extends Disposable {
private readonly _onError = this._register(new Emitter<Error>());
public readonly onError = this._onError.event;
private readonly _zlibInflate: zlib.InflateRaw;
private readonly _recordedInflateBytes: VSBuffer[] = [];
private readonly _pendingInflateData: VSBuffer[] = [];
public get recordedInflateBytes(): VSBuffer {
if (this._recordInflateBytes) {
return VSBuffer.concat(this._recordedInflateBytes);
}
return VSBuffer.alloc(0);
}
constructor(
private readonly _tracer: ISocketTracer,
private readonly _recordInflateBytes: boolean,
inflateBytes: VSBuffer | null,
options: zlib.ZlibOptions
) {
super();
this._zlibInflate = zlib.createInflateRaw(options);
this._zlibInflate.on('error', (err) => {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibInflateError, { message: err?.message, code: (<any>err)?.code });
this._onError.fire(err);
});
this._zlibInflate.on('data', (data: Buffer) => {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibInflateData, data);
this._pendingInflateData.push(VSBuffer.wrap(data));
});
if (inflateBytes) {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibInflateInitialWrite, inflateBytes.buffer);
this._zlibInflate.write(inflateBytes.buffer);
this._zlibInflate.flush(() => {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibInflateInitialFlushFired);
this._pendingInflateData.length = 0;
});
}
}
public write(buffer: VSBuffer): void {
if (this._recordInflateBytes) {
this._recordedInflateBytes.push(buffer.clone());
}
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibInflateWrite, buffer);
this._zlibInflate.write(buffer.buffer);
}
public flush(callback: (data: VSBuffer) => void): void {
this._zlibInflate.flush(() => {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibInflateFlushFired);
const data = VSBuffer.concat(this._pendingInflateData);
this._pendingInflateData.length = 0;
callback(data);
});
}
}
class ZlibDeflateStream extends Disposable {
private readonly _onError = this._register(new Emitter<Error>());
public readonly onError = this._onError.event;
private readonly _zlibDeflate: zlib.DeflateRaw;
private readonly _pendingDeflateData: VSBuffer[] = [];
constructor(
private readonly _tracer: ISocketTracer,
options: zlib.ZlibOptions
) {
super();
this._zlibDeflate = zlib.createDeflateRaw({
windowBits: 15
});
this._zlibDeflate.on('error', (err) => {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibDeflateError, { message: err?.message, code: (<any>err)?.code });
this._onError.fire(err);
});
this._zlibDeflate.on('data', (data: Buffer) => {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibDeflateData, data);
this._pendingDeflateData.push(VSBuffer.wrap(data));
});
}
public write(buffer: VSBuffer): void {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibDeflateWrite, buffer.buffer);
this._zlibDeflate.write(<Buffer>buffer.buffer);
}
public flush(callback: (data: VSBuffer) => void): void {
// See https://zlib.net/manual.html#Constants
this._zlibDeflate.flush(/*Z_SYNC_FLUSH*/2, () => {
this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibDeflateFlushFired);
let data = VSBuffer.concat(this._pendingDeflateData);
this._pendingDeflateData.length = 0;
// See https://tools.ietf.org/html/rfc7692#section-7.2.1
data = data.slice(0, data.byteLength - 4);
callback(data);
});
}
}
@ -597,7 +760,7 @@ export class Server extends IPCServer {
const onConnection = Event.fromNodeEventEmitter<Socket>(server, 'connection');
return Event.map(onConnection, socket => ({
protocol: new Protocol(new NodeSocket(socket)),
protocol: new Protocol(new NodeSocket(socket, 'ipc-server-connection')),
onDidClientDisconnect: Event.once(Event.fromNodeEventEmitter<void>(socket, 'close'))
}));
}
@ -639,7 +802,7 @@ export function connect(hook: any, clientId: string): Promise<Client> {
return new Promise<Client>((c, e) => {
const socket = createConnection(hook, () => {
socket.removeListener('error', e);
c(Client.fromSocket(new NodeSocket(socket), clientId));
c(Client.fromSocket(new NodeSocket(socket, `ipc-client${clientId}`), clientId));
});
socket.once('error', e);

View file

@ -11,7 +11,7 @@ import { Barrier, timeout } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ILoadEstimator, PersistentProtocol, Protocol, ProtocolConstants, SocketCloseEvent } from 'vs/base/parts/ipc/common/ipc.net';
import { ILoadEstimator, PersistentProtocol, Protocol, ProtocolConstants, SocketCloseEvent, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net';
import { createRandomIPCHandle, createStaticIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
@ -342,7 +342,7 @@ suite('PersistentProtocol reconnection', () => {
assert.strictEqual(b.unacknowledgedCount, 1);
// wait for scheduled _recvAckCheck() to execute
await timeout(2 * ProtocolConstants.AcknowledgeTimeoutTime);
await timeout(2 * ProtocolConstants.TimeoutTime);
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 1);
@ -351,7 +351,7 @@ suite('PersistentProtocol reconnection', () => {
a.endAcceptReconnection();
assert.strictEqual(timeoutListenerCalled, false);
await timeout(2 * ProtocolConstants.AcknowledgeTimeoutTime);
await timeout(2 * ProtocolConstants.TimeoutTime);
assert.strictEqual(a.unacknowledgedCount, 0);
assert.strictEqual(b.unacknowledgedCount, 0);
assert.strictEqual(timeoutListenerCalled, false);
@ -364,6 +364,59 @@ suite('PersistentProtocol reconnection', () => {
}
);
});
test('writing can be paused', async () => {
await runWithFakedTimers({ useFakeTimers: true, maxTaskCount: 100 }, async () => {
const loadEstimator: ILoadEstimator = {
hasHighLoad: () => false
};
const ether = new Ether();
const aSocket = new NodeSocket(ether.a);
const a = new PersistentProtocol(aSocket, null, loadEstimator);
const aMessages = new MessageStream(a);
const bSocket = new NodeSocket(ether.b);
const b = new PersistentProtocol(bSocket, null, loadEstimator);
const bMessages = new MessageStream(b);
// send one message A -> B
a.send(VSBuffer.fromString('a1'));
const a1 = await bMessages.waitForOne();
assert.strictEqual(a1.toString(), 'a1');
// ask A to pause writing
b.sendPause();
// send a message B -> A
b.send(VSBuffer.fromString('b1'));
const b1 = await aMessages.waitForOne();
assert.strictEqual(b1.toString(), 'b1');
// send a message A -> B (this should be blocked at A)
a.send(VSBuffer.fromString('a2'));
// wait a long time and check that not even acks are written
await timeout(2 * ProtocolConstants.AcknowledgeTime);
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 1);
// ask A to resume writing
b.sendResume();
// check that B receives message
const a2 = await bMessages.waitForOne();
assert.strictEqual(a2.toString(), 'a2');
// wait a long time and check that acks are written
await timeout(2 * ProtocolConstants.AcknowledgeTime);
assert.strictEqual(a.unacknowledgedCount, 0);
assert.strictEqual(b.unacknowledgedCount, 0);
aMessages.dispose();
bMessages.dispose();
a.dispose();
b.dispose();
});
});
});
suite('IPC, create handle', () => {
@ -432,6 +485,9 @@ suite('WebSocketNodeSocket', () => {
private readonly _onClose = new Emitter<SocketCloseEvent>();
public readonly onClose = this._onClose.event;
public traceSocketEvent(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void {
}
constructor() {
super();
}
@ -522,5 +578,14 @@ suite('WebSocketNodeSocket', () => {
const actual = await testReading(frames, true);
assert.deepStrictEqual(actual, 'Hello');
});
test('A single-frame compressed text message followed by a single-frame non-compressed text message', async () => {
const frames = [
[0xc1, 0x07, 0xf2, 0x48, 0xcd, 0xc9, 0xc9, 0x07, 0x00], // contains "Hello"
[0x81, 0x05, 0x77, 0x6f, 0x72, 0x6c, 0x64] // contains "world"
];
const actual = await testReading(frames, true);
assert.deepStrictEqual(actual, 'Helloworld');
});
});
});

View file

@ -41,7 +41,7 @@ import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encry
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
import { getResolvedShellEnv } from 'vs/platform/environment/node/shellEnv';
import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust';
import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService';
import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter';
@ -1033,7 +1033,7 @@ export class CodeApplication extends Disposable {
private async resolveShellEnvironment(args: NativeParsedArgs, env: IProcessEnvironment, notifyOnError: boolean): Promise<typeof process.env> {
try {
return await resolveShellEnv(this.logService, args, env);
return await getResolvedShellEnv(this.logService, args, env);
} catch (error) {
const errorMessage = toErrorMessage(error);
if (notifyOnError) {

View file

@ -22,7 +22,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { IFileService } from 'vs/platform/files/common/files';
@ -217,7 +217,8 @@ class CliMain extends Disposable {
// Install Extension
else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) {
return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.argv['install-builtin-extension'] || [], !!this.argv['do-not-sync'], !!this.argv['force']);
const installOptions: InstallOptions = { isMachineScoped: !!this.argv['do-not-sync'], installPreReleaseVersion: !!this.argv['pre-release'] };
return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.argv['install-builtin-extension'] || [], installOptions, !!this.argv['force']);
}
// Uninstall Extension

View file

@ -899,6 +899,8 @@ export interface ICodeEditor extends editorCommon.IEditor {
* @internal
*/
hasModel(): this is IActiveCodeEditor;
setBanner(bannerDomNode: HTMLElement | null, height: number): void;
}
/**

View file

@ -244,6 +244,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
private _decorationTypeKeysToIds: { [decorationTypeKey: string]: string[] };
private _decorationTypeSubtypes: { [decorationTypeKey: string]: { [subtype: string]: boolean } };
private _bannerDomNode: HTMLElement | null = null;
constructor(
domElement: HTMLElement,
_options: Readonly<editorBrowser.IEditorConstructionOptions>,
@ -1490,6 +1492,19 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
Configuration.applyFontInfoSlow(target, this._configuration.options.get(EditorOption.fontInfo));
}
public setBanner(domNode: HTMLElement | null, domNodeHeight: number): void {
if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) {
this._domElement.removeChild(this._bannerDomNode);
}
this._bannerDomNode = domNode;
this._configuration.reserveHeight(domNode ? domNodeHeight : 0);
if (this._bannerDomNode) {
this._domElement.prepend(this._bannerDomNode);
}
}
protected _attachModel(model: ITextModel | null): void {
if (!model) {
this._modelData = null;
@ -1703,6 +1718,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
if (removeDomNode && this._domElement.contains(removeDomNode)) {
this._domElement.removeChild(removeDomNode);
}
if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) {
this._domElement.removeChild(this._bannerDomNode);
}
return model;
}

View file

@ -313,6 +313,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC
private _rawOptions: IEditorOptions;
private _readOptions: RawEditorOptions;
protected _validatedOptions: ValidatedEditorOptions;
private _reservedHeight: number = 0;
constructor(isSimpleWidget: boolean, _options: Readonly<IEditorOptions>) {
super();
@ -367,7 +368,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC
const env: IEnvironmentalOptions = {
memory: this._computeOptionsMemory,
outerWidth: partialEnv.outerWidth,
outerHeight: partialEnv.outerHeight,
outerHeight: partialEnv.outerHeight - this._reservedHeight,
fontInfo: this.readConfiguration(bareFontInfo),
extraEditorClassName: partialEnv.extraEditorClassName,
isDominatedByLongLines: this._isDominatedByLongLines,
@ -458,6 +459,10 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC
protected abstract readConfiguration(styling: BareFontInfo): FontInfo;
public reserveHeight(height: number) {
this._reservedHeight = height;
this._recomputeOptions();
}
}
export const editorConfigurationBaseNode = Object.freeze<IConfigurationNode>({

View file

@ -162,6 +162,7 @@ export interface IConfiguration extends IDisposable {
observeReferenceElement(dimension?: IDimension): void;
updatePixelRatio(): void;
setIsDominatedByLongLines(isDominatedByLongLines: boolean): void;
reserveHeight(height: number): void;
}
// --- view

View file

@ -760,7 +760,7 @@ export interface ITextModel {
getLineLastNonWhitespaceColumn(lineNumber: number): number;
/**
* Create a valid position,
* Create a valid position.
*/
validatePosition(position: IPosition): Position;
@ -800,7 +800,7 @@ export interface ITextModel {
getPositionAt(offset: number): Position;
/**
* Get a range covering the entire model
* Get a range covering the entire model.
*/
getFullModelRange(): Range;

View file

@ -539,11 +539,10 @@ export interface CompletionItem {
/**
* A string or snippet that should be inserted in a document when selecting
* this completion.
* is used.
*/
insertText: string;
/**
* Addition rules (as bitmask) that should be applied when inserting
* Additional rules (as bitmask) that should be applied when inserting
* this completion.
*/
insertTextRules?: CompletionItemInsertTextRule;

View file

@ -10,6 +10,7 @@ import { ILanguageExtensionPoint } from 'vs/editor/common/services/modeService';
import { Registry } from 'vs/platform/registry/common/platform';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Mimes } from 'vs/base/common/mime';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
// Define extension point ids
export const Extensions = {
@ -86,3 +87,12 @@ LanguageConfigurationRegistry.register(PLAINTEXT_MODE_ID, {
offSide: true
}
}, 0);
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
.registerDefaultConfigurations([{
overrides: {
'[plaintext]': {
'editor.unicodeHighlight.ambiguousCharacters': false
}
}
}]);

View file

@ -6,9 +6,11 @@
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';
import { IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService';
import { assertNever } from 'vs/base/common/types';
export class UnicodeTextModelHighlighter {
public static computeUnicodeHighlights(model: IUnicodeCharacterSearcherTarget, options: UnicodeHighlighterOptions, range?: IRange): Range[] {
public static computeUnicodeHighlights(model: IUnicodeCharacterSearcherTarget, options: UnicodeHighlighterOptions, range?: IRange): IUnicodeHighlightsResult {
const startLine = range ? range.startLineNumber : 1;
const endLine = range ? range.endLineNumber : model.getLineCount();
@ -23,8 +25,15 @@ export class UnicodeTextModelHighlighter {
}
const searcher = new Searcher(null, regex);
const result: Range[] = [];
const ranges: Range[] = [];
let hasMore = false;
let m: RegExpExecArray | null;
let ambiguousCharacterCount = 0;
let invisibleCharacterCount = 0;
let nonBasicAsciiCharacterCount = 0;
forLoop:
for (let lineNumber = startLine, lineCount = endLine; lineNumber <= lineCount; lineNumber++) {
const lineContent = model.getLineContent(lineNumber);
const lineLength = lineContent.length;
@ -51,19 +60,37 @@ export class UnicodeTextModelHighlighter {
}
}
const str = lineContent.substring(startIndex, endIndex);
if (codePointHighlighter.shouldHighlightNonBasicASCII(str) !== SimpleHighlightReason.None) {
result.push(new Range(lineNumber, startIndex + 1, lineNumber, endIndex + 1));
const highlightReason = codePointHighlighter.shouldHighlightNonBasicASCII(str);
const maxResultLength = 1000;
if (result.length > maxResultLength) {
// TODO@hediet a message should be shown in this case
break;
if (highlightReason !== SimpleHighlightReason.None) {
if (highlightReason === SimpleHighlightReason.Ambiguous) {
ambiguousCharacterCount++;
} else if (highlightReason === SimpleHighlightReason.Invisible) {
invisibleCharacterCount++;
} else if (highlightReason === SimpleHighlightReason.NonBasicASCII) {
nonBasicAsciiCharacterCount++;
} else {
assertNever(highlightReason);
}
const MAX_RESULT_LENGTH = 1000;
if (ranges.length >= MAX_RESULT_LENGTH) {
hasMore = true;
break forLoop;
}
ranges.push(new Range(lineNumber, startIndex + 1, lineNumber, endIndex + 1));
}
}
} while (m);
}
return result;
return {
ranges,
hasMore,
ambiguousCharacterCount,
invisibleCharacterCount,
nonBasicAsciiCharacterCount
};
}
public static computeUnicodeHighlightReason(char: string, options: UnicodeHighlighterOptions): UnicodeHighlighterReason | null {

View file

@ -18,7 +18,7 @@ import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/model
import { IInplaceReplaceSupportResult, ILink, TextEdit } from 'vs/editor/common/modes';
import { ILinkComputerTarget, computeLinks } from 'vs/editor/common/modes/linkComputer';
import { BasicInplaceReplace } from 'vs/editor/common/modes/supports/inplaceReplaceSupport';
import { IDiffComputationResult } from 'vs/editor/common/services/editorWorkerService';
import { IDiffComputationResult, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService';
import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase';
import * as types from 'vs/base/common/types';
import { EditorWorkerHost } from 'vs/editor/common/services/editorWorkerServiceImpl';
@ -372,10 +372,10 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
delete this._models[strURL];
}
public async computeUnicodeHighlights(url: string, options: UnicodeHighlighterOptions, range?: IRange): Promise<IRange[]> {
public async computeUnicodeHighlights(url: string, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {
const model = this._getModel(url);
if (!model) {
return [];
return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 };
}
return UnicodeTextModelHighlighter.computeUnicodeHighlights(model, options, range);
}

View file

@ -23,7 +23,7 @@ export interface IEditorWorkerService {
readonly _serviceBrand: undefined;
canComputeUnicodeHighlights(uri: URI): boolean;
computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IRange[]>;
computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult>;
computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null>;
@ -38,3 +38,11 @@ export interface IEditorWorkerService {
canNavigateValueSet(resource: URI): boolean;
navigateValueSet(resource: URI, range: IRange, up: boolean): Promise<IInplaceReplaceSupportResult | null>;
}
export interface IUnicodeHighlightsResult {
ranges: IRange[];
hasMore: boolean;
nonBasicAsciiCharacterCount: number;
invisibleCharacterCount: number;
ambiguousCharacterCount: number;
}

View file

@ -15,7 +15,7 @@ import { ITextModel } from 'vs/editor/common/model';
import * as modes from 'vs/editor/common/modes';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker';
import { IDiffComputationResult, IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
import { regExpFlags } from 'vs/base/common/strings';
@ -86,7 +86,7 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker
return canSyncModel(this._modelService, uri);
}
public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IRange[]> {
public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {
return this._workerManager.withWorker().then(client => client.computedUnicodeHighlights(uri, options, range));
}
@ -475,7 +475,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien
});
}
public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IRange[]> {
public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult> {
return this._withSyncedResources([uri]).then(proxy => {
return proxy.computeUnicodeHighlights(uri.toString(), options, range);
});

View file

@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.editor-banner {
box-sizing: border-box;
cursor: default;
width: 100%;
font-size: 12px;
display: flex;
overflow: visible;
height: 26px;
background: var(--vscode-banner-background);
}
.editor-banner .icon-container {
display: flex;
flex-shrink: 0;
align-items: center;
padding: 0 6px 0 10px;
}
.editor-banner .icon-container.custom-icon {
background-repeat: no-repeat;
background-position: center center;
background-size: 16px;
width: 16px;
padding: 0;
margin: 0 6px 0 10px;
}
.editor-banner .message-container {
display: flex;
align-items: center;
line-height: 26px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.editor-banner .message-container p {
margin-block-start: 0;
margin-block-end: 0;
}
.editor-banner .message-actions-container {
flex-grow: 1;
flex-shrink: 0;
line-height: 26px;
margin: 0 4px;
}
.editor-banner .message-actions-container a.monaco-button {
width: inherit;
margin: 2px 8px;
padding: 0px 12px;
}
.editor-banner .message-actions-container a {
padding: 3px;
margin-left: 12px;
text-decoration: underline;
}
.editor-banner .action-container {
padding: 0 10px 0 6px;
}
.editor-banner {
background-color: var(--vscode-banner-background);
}
.editor-banner,
.editor-banner .action-container .codicon,
.editor-banner .message-actions-container .monaco-link {
color: var(--vscode-banner-foreground);
}
.editor-banner .icon-container .codicon {
color: var(--vscode-banner-iconForeground);
}

View file

@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./bannerController';
import { $, append, clearNode } from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable } from 'vs/base/common/lifecycle';
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILinkDescriptor, Link } from 'vs/platform/opener/browser/link';
import { widgetClose } from 'vs/platform/theme/common/iconRegistry';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
const BANNER_ELEMENT_HEIGHT = 26;
export class BannerController extends Disposable {
private readonly banner: Banner;
constructor(
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.banner = this._register(this.instantiationService.createInstance(Banner));
}
public hide() {
this._editor.setBanner(null, 0);
this.banner.clear();
}
public show(item: IBannerItem) {
this.banner.show({
...item,
onClose: () => {
this.hide();
if (item.onClose) {
item.onClose();
}
}
});
this._editor.setBanner(this.banner.element, BANNER_ELEMENT_HEIGHT);
}
}
// TODO@hediet: Investigate if this can be reused by the workspace banner (bannerPart.ts).
class Banner extends Disposable {
public element: HTMLElement;
private readonly markdownRenderer: MarkdownRenderer;
private messageActionsContainer: HTMLElement | undefined;
private actionBar: ActionBar | undefined;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
this.element = $('div.editor-banner');
this.element.tabIndex = 0;
}
private getAriaLabel(item: IBannerItem): string | undefined {
if (item.ariaLabel) {
return item.ariaLabel;
}
if (typeof item.message === 'string') {
return item.message;
}
return undefined;
}
private getBannerMessage(message: MarkdownString | string): HTMLElement {
if (typeof message === 'string') {
const element = $('span');
element.innerText = message;
return element;
}
return this.markdownRenderer.render(message).element;
}
public clear() {
clearNode(this.element);
}
public show(item: IBannerItem) {
// Clear previous item
clearNode(this.element);
// Banner aria label
const ariaLabel = this.getAriaLabel(item);
if (ariaLabel) {
this.element.setAttribute('aria-label', ariaLabel);
}
// Icon
const iconContainer = append(this.element, $('div.icon-container'));
iconContainer.setAttribute('aria-hidden', 'true');
if (item.icon) {
iconContainer.appendChild($(`div${ThemeIcon.asCSSSelector(item.icon)}`));
}
// Message
const messageContainer = append(this.element, $('div.message-container'));
messageContainer.setAttribute('aria-hidden', 'true');
messageContainer.appendChild(this.getBannerMessage(item.message));
// Message Actions
this.messageActionsContainer = append(this.element, $('div.message-actions-container'));
if (item.actions) {
for (const action of item.actions) {
this._register(this.instantiationService.createInstance(Link, this.messageActionsContainer, { ...action, tabIndex: -1 }, {}));
}
}
// Action
const actionBarContainer = append(this.element, $('div.action-container'));
this.actionBar = this._register(new ActionBar(actionBarContainer));
this.actionBar.push(this._register(
new Action(
'banner.close',
'Close Banner',
ThemeIcon.asClassName(widgetClose),
true,
() => {
if (typeof item.onClose === 'function') {
item.onClose();
}
}
)
), { icon: true, label: false });
this.actionBar.setFocusable(false);
}
}
export interface IBannerItem {
readonly id: string;
readonly icon: ThemeIcon | undefined;
readonly message: string | MarkdownString;
readonly actions?: ILinkDescriptor[];
readonly ariaLabel?: string;
readonly onClose?: () => void;
}

View file

@ -5,6 +5,7 @@
import { RunOnceScheduler } from 'vs/base/common/async';
import { CharCode } from 'vs/base/common/charCode';
import { Codicon } from 'vs/base/common/codicons';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { InvisibleCharacters } from 'vs/base/common/strings';
@ -17,32 +18,44 @@ 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 { IEditorWorkerService, IUnicodeHighlightsResult } 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 { BannerController } from 'vs/editor/contrib/unicodeHighlighter/bannerController';
import * as nls from 'vs/nls';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
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 { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
export const warningIcon = registerIcon('extensions-warning-message', Codicon.warning, nls.localize('warningIcon', 'Icon shown with a warning message in the extensions editor.'));
export class UnicodeHighlighter extends Disposable implements IEditorContribution {
public static readonly ID = 'editor.contrib.unicodeHighlighter';
private _highlighter: DocumentUnicodeHighlighter | ViewportUnicodeHighlighter | null = null;
private _options: InternalUnicodeHighlightOptions;
private readonly _bannerController: BannerController;
private _bannerClosed: boolean = false;
constructor(
private readonly _editor: ICodeEditor,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@IWorkspaceTrustManagementService private readonly _workspaceTrustService: IWorkspaceTrustManagementService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
this._bannerController = this._register(instantiationService.createInstance(BannerController, _editor));
this._register(this._editor.onDidChangeModel(() => {
this._bannerClosed = false;
this._updateHighlighter();
}));
@ -70,7 +83,57 @@ export class UnicodeHighlighter extends Disposable implements IEditorContributio
super.dispose();
}
private readonly _updateState = (state: IUnicodeHighlightsResult | null): void => {
if (state && state.hasMore) {
if (this._bannerClosed) {
return;
}
// This document contains many non-basic ASCII characters.
const max = Math.max(state.ambiguousCharacterCount, state.nonBasicAsciiCharacterCount, state.invisibleCharacterCount);
let data;
if (state.nonBasicAsciiCharacterCount >= max) {
data = {
message: nls.localize('unicodeHighlighting.thisDocumentHasManyNonBasicAsciiUnicodeCharacters', 'This document contains many non-basic ASCII unicode characters'),
command: new DisableHighlightingOfNonBasicAsciiCharactersAction(),
};
} else if (state.ambiguousCharacterCount >= max) {
data = {
message: nls.localize('unicodeHighlighting.thisDocumentHasManyAmbiguousUnicodeCharacters', 'This document contains many ambiguous unicode characters'),
command: new DisableHighlightingOfAmbiguousCharactersAction(),
};
} else if (state.invisibleCharacterCount >= max) {
data = {
message: nls.localize('unicodeHighlighting.thisDocumentHasManyInvisibleUnicodeCharacters', 'This document contains many invisible unicode characters'),
command: new DisableHighlightingOfInvisibleCharactersAction(),
};
} else {
throw new Error('Unreachable');
}
this._bannerController.show({
id: 'unicodeHighlightBanner',
message: data.message,
icon: warningIcon,
actions: [
{
label: data.command.shortLabel,
href: `command:${data.command.id}`
}
],
onClose: () => {
this._bannerClosed = true;
},
});
} else {
this._bannerController.hide();
}
};
private _updateHighlighter(): void {
this._updateState(null);
if (this._highlighter) {
this._highlighter.dispose();
this._highlighter = null;
@ -100,9 +163,9 @@ export class UnicodeHighlighter extends Disposable implements IEditorContributio
};
if (this._editorWorkerService.canComputeUnicodeHighlights(this._editor.getModel().uri)) {
this._highlighter = new DocumentUnicodeHighlighter(this._editor, highlightOptions, this._editorWorkerService);
this._highlighter = new DocumentUnicodeHighlighter(this._editor, highlightOptions, this._updateState, this._editorWorkerService);
} else {
this._highlighter = new ViewportUnicodeHighlighter(this._editor, highlightOptions);
this._highlighter = new ViewportUnicodeHighlighter(this._editor, highlightOptions, this._updateState);
}
}
@ -156,6 +219,7 @@ class DocumentUnicodeHighlighter extends Disposable {
constructor(
private readonly _editor: IActiveCodeEditor,
private readonly _options: UnicodeHighlighterOptions,
private readonly _updateState: (state: IUnicodeHighlightsResult | null) => void,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
) {
super();
@ -182,14 +246,20 @@ class DocumentUnicodeHighlighter extends Disposable {
const modelVersionId = this._model.getVersionId();
this._editorWorkerService
.computedUnicodeHighlights(this._model.uri, this._options)
.then((ranges) => {
.then((info) => {
if (this._model.getVersionId() !== modelVersionId) {
// model changed in the meantime
return;
}
this._updateState(info);
const decorations: IModelDeltaDecoration[] = [];
for (const range of ranges) {
decorations.push({ range: range, options: this._options.includeComments ? DECORATION : DECORATION_HIDE_IN_COMMENTS });
if (!info.hasMore) {
// Don't show decoration if there are too many.
// In this case, a banner is shown.
for (const range of info.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),
@ -218,7 +288,8 @@ class ViewportUnicodeHighlighter extends Disposable {
constructor(
private readonly _editor: IActiveCodeEditor,
private readonly _options: UnicodeHighlighterOptions
private readonly _options: UnicodeHighlighterOptions,
private readonly _updateState: (state: IUnicodeHighlightsResult | null) => void,
) {
super();
@ -253,12 +324,33 @@ class ViewportUnicodeHighlighter extends Disposable {
const ranges = this._editor.getVisibleRanges();
const decorations: IModelDeltaDecoration[] = [];
const totalResult: IUnicodeHighlightsResult = {
ranges: [],
ambiguousCharacterCount: 0,
invisibleCharacterCount: 0,
nonBasicAsciiCharacterCount: 0,
hasMore: false,
};
for (const range of ranges) {
const ranges = UnicodeTextModelHighlighter.computeUnicodeHighlights(this._model, this._options, range);
for (const range of ranges) {
const result = UnicodeTextModelHighlighter.computeUnicodeHighlights(this._model, this._options, range);
for (const r of result.ranges) {
totalResult.ranges.push(r);
}
totalResult.ambiguousCharacterCount += totalResult.ambiguousCharacterCount;
totalResult.invisibleCharacterCount += totalResult.invisibleCharacterCount;
totalResult.nonBasicAsciiCharacterCount += totalResult.nonBasicAsciiCharacterCount;
totalResult.hasMore = totalResult.hasMore || result.hasMore;
}
if (!totalResult.hasMore) {
// Don't show decorations if there are too many.
// A banner will be shown instead.
for (const range of totalResult.ranges) {
decorations.push({ range, options: this._options.includeComments ? DECORATION : DECORATION_HIDE_IN_COMMENTS });
}
}
this._updateState(totalResult);
this._decorationIds = new Set(this._editor.deltaDecorations(Array.from(this._decorationIds), decorations));
}
@ -356,7 +448,7 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa
reason = nls.localize(
'unicodeHighlight.characterIsNonBasicAscii',
'The character {0} is not a basic ASCII character.',
codePoint
codePointStr
);
break;
}
@ -424,6 +516,82 @@ const DECORATION = ModelDecorationOptions.register({
}
});
interface IDisableUnicodeHighlightAction {
shortLabel: string;
}
export class DisableHighlightingOfAmbiguousCharactersAction extends EditorAction implements IDisableUnicodeHighlightAction {
public static ID = 'editor.action.unicodeHighlight.disableHighlightingOfAmbiguousCharacters';
public readonly shortLabel = nls.localize('unicodeHighlight.disableHighlightingOfAmbiguousCharacters.shortLabel', '');
constructor() {
super({
id: DisableHighlightingOfAmbiguousCharactersAction.ID,
label: nls.localize('action.unicodeHighlight.disableHighlightingOfAmbiguousCharacters', 'Disable Ambiguous Highlight'),
alias: 'Disable highlighting of ambiguous characters',
precondition: undefined
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise<void> {
let configurationService = accessor?.get(IConfigurationService);
if (configurationService) {
this.runAction(configurationService);
}
}
public async runAction(configurationService: IConfigurationService): Promise<void> {
await configurationService.updateValue(unicodeHighlightConfigKeys.ambiguousCharacters, false, ConfigurationTarget.USER);
}
}
export class DisableHighlightingOfInvisibleCharactersAction extends EditorAction implements IDisableUnicodeHighlightAction {
public static ID = 'editor.action.unicodeHighlight.disableHighlightingOfInvisibleCharacters';
public readonly shortLabel = nls.localize('unicodeHighlight.disableHighlightingOfInvisibleCharacters.shortLabel', 'Disable Invisible Highlight');
constructor() {
super({
id: DisableHighlightingOfInvisibleCharactersAction.ID,
label: nls.localize('action.unicodeHighlight.disableHighlightingOfInvisibleCharacters', 'Disable highlighting of invisible characters'),
alias: 'Disable highlighting of invisible characters',
precondition: undefined
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise<void> {
let configurationService = accessor?.get(IConfigurationService);
if (configurationService) {
this.runAction(configurationService);
}
}
public async runAction(configurationService: IConfigurationService): Promise<void> {
await configurationService.updateValue(unicodeHighlightConfigKeys.invisibleCharacters, false, ConfigurationTarget.USER);
}
}
export class DisableHighlightingOfNonBasicAsciiCharactersAction extends EditorAction implements IDisableUnicodeHighlightAction {
public static ID = 'editor.action.unicodeHighlight.disableHighlightingOfNonBasicAsciiCharacters';
public readonly shortLabel = nls.localize('unicodeHighlight.disableHighlightingOfNonBasicAsciiCharacters.shortLabel', 'Disable Non ASCII Highlight');
constructor() {
super({
id: DisableHighlightingOfNonBasicAsciiCharactersAction.ID,
label: nls.localize('action.unicodeHighlight.dhowDisableHighlightingOfNonBasicAsciiCharacters', 'Disable highlighting of non basic ASCII characters'),
alias: 'Disable highlighting of non basic ASCII characters',
precondition: undefined
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise<void> {
let configurationService = accessor?.get(IConfigurationService);
if (configurationService) {
this.runAction(configurationService);
}
}
public async runAction(configurationService: IConfigurationService): Promise<void> {
await configurationService.updateValue(unicodeHighlightConfigKeys.nonBasicASCII, false, ConfigurationTarget.USER);
}
}
interface ShowExcludeOptionsArgs {
codePoint: number;
reason: UnicodeHighlighterReason['kind'];
@ -439,6 +607,7 @@ export class ShowExcludeOptions extends EditorAction {
precondition: undefined
});
}
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise<void> {
const { codePoint, reason } = args as ShowExcludeOptionsArgs;
@ -470,28 +639,16 @@ export class ShowExcludeOptions extends EditorAction {
];
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);
}
});
const action = new DisableHighlightingOfAmbiguousCharactersAction();
options.push({ label: action.label, run: async () => action.runAction(configurationService) });
}
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);
}
});
const action = new DisableHighlightingOfInvisibleCharactersAction();
options.push({ label: action.label, run: async () => action.runAction(configurationService) });
}
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);
}
});
const action = new DisableHighlightingOfNonBasicAsciiCharactersAction();
options.push({ label: action.label, run: async () => action.runAction(configurationService) });
} else {
expectNever(reason);
}
@ -511,5 +668,8 @@ function expectNever(value: never) {
throw new Error(`Unexpected value: ${value}`);
}
registerEditorAction(DisableHighlightingOfAmbiguousCharactersAction);
registerEditorAction(DisableHighlightingOfInvisibleCharactersAction);
registerEditorAction(DisableHighlightingOfNonBasicAsciiCharactersAction);
registerEditorAction(ShowExcludeOptions);
registerEditorContribution(UnicodeHighlighter.ID, UnicodeHighlighter);

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

@ -1807,7 +1807,7 @@ declare namespace monaco.editor {
*/
getLineLastNonWhitespaceColumn(lineNumber: number): number;
/**
* Create a valid position,
* Create a valid position.
*/
validatePosition(position: IPosition): Position;
/**
@ -1842,7 +1842,7 @@ declare namespace monaco.editor {
*/
getPositionAt(offset: number): Position;
/**
* Get a range covering the entire model
* Get a range covering the entire model.
*/
getFullModelRange(): Range;
/**
@ -5117,6 +5117,7 @@ declare namespace monaco.editor {
* Apply the same font settings as the editor to `target`.
*/
applyFontInfo(target: HTMLElement): void;
setBanner(bannerDomNode: HTMLElement | null, height: number): void;
}
/**
@ -5898,11 +5899,10 @@ declare namespace monaco.languages {
/**
* A string or snippet that should be inserted in a document when selecting
* this completion.
* is used.
*/
insertText: string;
/**
* Addition rules (as bitmask) that should be applied when inserting
* Additional rules (as bitmask) that should be applied when inserting
* this completion.
*/
insertTextRules?: CompletionItemInsertTextRule;

View file

@ -48,17 +48,17 @@ export interface IConfigurationRegistry {
/**
* Register multiple default configurations to the registry.
*/
registerDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void;
registerDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
/**
* Deregister multiple default configurations from the registry.
*/
deregisterDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void;
deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
/**
* Return the registered configuration defaults overrides
*/
getConfigurationDefaultsOverrides(): IStringDictionary<any>;
getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverride>;
/**
* Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values.
@ -86,12 +86,12 @@ export interface IConfigurationRegistry {
/**
* Returns all configurations settings of all configuration nodes contributed to this registry.
*/
getConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema };
getConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema>;
/**
* Returns all excluded configurations settings of all configuration nodes contributed to this registry.
*/
getExcludedConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema };
getExcludedConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema>;
/**
* Register the identifiers for editor configurations
@ -136,8 +136,16 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
*/
restricted?: boolean;
/**
* When `false` this property is excluded from the registry. Default is to include.
*/
included?: boolean;
/**
* List of tags associated to the property.
* - A tag can be used for filtering
* - Use `experimental` tag for marking the setting as experimental. **Note:** Defaults of experimental settings can be changed by the running experiments.
*/
tags?: string[];
/**
@ -150,6 +158,9 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
*/
disallowSyncIgnore?: boolean;
/**
* Labels for enumeration items
*/
enumItemLabels?: string[];
/**
@ -165,9 +176,9 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
order?: number;
}
export interface IConfigurationExtensionInfo {
export interface IExtensionInfo {
id: string;
restrictedConfigurations?: string[];
displayName?: string;
}
export interface IConfigurationNode {
@ -176,12 +187,26 @@ export interface IConfigurationNode {
type?: string | string[];
title?: string;
description?: string;
properties?: { [path: string]: IConfigurationPropertySchema; };
properties?: IStringDictionary<IConfigurationPropertySchema>;
allOf?: IConfigurationNode[];
scope?: ConfigurationScope;
extensionInfo?: IConfigurationExtensionInfo;
extensionInfo?: IExtensionInfo;
restrictedProperties?: string[];
}
export interface IConfigurationDefaults {
overrides: IStringDictionary<any>;
source?: IExtensionInfo | string;
}
export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & {
defaultDefaultValue?: any,
source?: IExtensionInfo,
defaultValueSource?: IExtensionInfo | string;
};
export type IConfigurationDefaultOverride = { value: any, source?: IExtensionInfo | string };
export const allSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
export const applicationSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
export const machineSettings: { properties: IStringDictionary<IConfigurationPropertySchema>, patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
@ -195,11 +220,11 @@ const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensio
class ConfigurationRegistry implements IConfigurationRegistry {
private readonly configurationDefaultsOverrides: IStringDictionary<any>;
private readonly configurationDefaultsOverrides: Map<string, IConfigurationDefaultOverride>;
private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode;
private readonly configurationContributors: IConfigurationNode[];
private readonly configurationProperties: { [qualifiedKey: string]: IJSONSchema };
private readonly excludedConfigurationProperties: { [qualifiedKey: string]: IJSONSchema };
private readonly configurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>;
private readonly excludedConfigurationProperties: IStringDictionary<IRegisteredConfigurationPropertySchema>;
private readonly resourceLanguageSettingsSchema: IJSONSchema;
private readonly overrideIdentifiers = new Set<string>();
@ -210,7 +235,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
readonly onDidUpdateConfiguration = this._onDidUpdateConfiguration.event;
constructor() {
this.configurationDefaultsOverrides = {};
this.configurationDefaultsOverrides = new Map<string, IConfigurationDefaultOverride>();
this.defaultLanguageConfigurationOverridesNode = {
id: 'defaultOverrides',
title: nls.localize('defaultLanguageConfigurationOverrides.title', "Default Language Configuration Overrides"),
@ -255,27 +280,30 @@ class ConfigurationRegistry implements IConfigurationRegistry {
this._onDidUpdateConfiguration.fire({ properties: distinct(properties) });
}
public registerDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void {
public registerDefaultConfigurations(configurationDefaults: IConfigurationDefaults[]): void {
const properties: string[] = [];
const overrideIdentifiers: string[] = [];
for (const defaultConfiguration of defaultConfigurations) {
for (const key in defaultConfiguration) {
for (const { overrides, source } of configurationDefaults) {
for (const key in overrides) {
properties.push(key);
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
this.configurationDefaultsOverrides[key] = { ...(this.configurationDefaultsOverrides[key] || {}), ...defaultConfiguration[key] };
const property: IConfigurationPropertySchema = {
const defaultValue = { ...(this.configurationDefaultsOverrides.get(key)?.value || {}), ...overrides[key] };
this.configurationDefaultsOverrides.set(key, { source, value: defaultValue });
const property: IRegisteredConfigurationPropertySchema = {
type: 'object',
default: this.configurationDefaultsOverrides[key],
default: defaultValue,
description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for {0} language.", key),
$ref: resourceLanguageSettingsSchemaId
$ref: resourceLanguageSettingsSchemaId,
defaultDefaultValue: defaultValue,
source: types.isString(source) ? undefined : source,
};
overrideIdentifiers.push(...overrideIdentifiersFromKey(key));
this.configurationProperties[key] = property;
this.defaultLanguageConfigurationOverridesNode.properties![key] = property;
} else {
this.configurationDefaultsOverrides[key] = defaultConfiguration[key];
this.configurationDefaultsOverrides.set(key, { value: overrides[key], source });
const property = this.configurationProperties[key];
if (property) {
this.updatePropertyDefaultValue(key, property);
@ -290,12 +318,18 @@ class ConfigurationRegistry implements IConfigurationRegistry {
this._onDidUpdateConfiguration.fire({ properties, defaultsOverrides: true });
}
public deregisterDefaultConfigurations(defaultConfigurations: IStringDictionary<any>[]): void {
public deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void {
const properties: string[] = [];
for (const defaultConfiguration of defaultConfigurations) {
for (const key in defaultConfiguration) {
for (const { overrides, source } of defaultConfigurations) {
for (const key in overrides) {
const configurationDefaultsOverride = this.configurationDefaultsOverrides.get(key);
const id = types.isString(source) ? source : source?.id;
const configurationDefaultsOverrideSourceId = types.isString(configurationDefaultsOverride?.source) ? configurationDefaultsOverride?.source : configurationDefaultsOverride?.source?.id;
if (id !== configurationDefaultsOverrideSourceId) {
continue;
}
properties.push(key);
delete this.configurationDefaultsOverrides[key];
this.configurationDefaultsOverrides.delete(key);
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
delete this.configurationProperties[key];
delete this.defaultLanguageConfigurationOverridesNode.properties![key];
@ -328,7 +362,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
private doRegisterConfigurations(configurations: IConfigurationNode[], validate: boolean): string[] {
const properties: string[] = [];
configurations.forEach(configuration => {
properties.push(...this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo)); // fills in defaults
properties.push(...this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo, configuration.restrictedProperties)); // fills in defaults
this.configurationContributors.push(configuration);
this.registerJSONConfiguration(configuration);
});
@ -359,7 +393,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
return properties;
}
private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, extensionInfo?: IConfigurationExtensionInfo, scope: ConfigurationScope = ConfigurationScope.WINDOW): string[] {
private validateAndRegisterProperties(configuration: IConfigurationNode, validate: boolean = true, extensionInfo: IExtensionInfo | undefined, restrictedProperties: string[] | undefined, scope: ConfigurationScope = ConfigurationScope.WINDOW): string[] {
scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope;
let propertyKeys: string[] = [];
let properties = configuration.properties;
@ -370,9 +404,11 @@ class ConfigurationRegistry implements IConfigurationRegistry {
continue;
}
const property = properties[key];
const property: IRegisteredConfigurationPropertySchema = properties[key];
property.source = extensionInfo;
// update default value
property.defaultDefaultValue = properties[key].default;
this.updatePropertyDefaultValue(key, property);
// update scope
@ -380,7 +416,7 @@ class ConfigurationRegistry implements IConfigurationRegistry {
property.scope = undefined; // No scope for overridable properties `[${identifier}]`
} else {
property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope;
property.restricted = types.isUndefinedOrNull(property.restricted) ? !!extensionInfo?.restrictedConfigurations?.includes(key) : property.restricted;
property.restricted = types.isUndefinedOrNull(property.restricted) ? !!restrictedProperties?.includes(key) : property.restricted;
}
// Add to properties maps
@ -404,25 +440,26 @@ class ConfigurationRegistry implements IConfigurationRegistry {
let subNodes = configuration.allOf;
if (subNodes) {
for (let node of subNodes) {
propertyKeys.push(...this.validateAndRegisterProperties(node, validate, extensionInfo, scope));
propertyKeys.push(...this.validateAndRegisterProperties(node, validate, extensionInfo, restrictedProperties, scope));
}
}
return propertyKeys;
}
// TODO: @sandy081 - Remove this method and include required info in getConfigurationProperties
getConfigurations(): IConfigurationNode[] {
return this.configurationContributors;
}
getConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema } {
getConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema> {
return this.configurationProperties;
}
getExcludedConfigurationProperties(): { [qualifiedKey: string]: IConfigurationPropertySchema } {
getExcludedConfigurationProperties(): IStringDictionary<IRegisteredConfigurationPropertySchema> {
return this.excludedConfigurationProperties;
}
getConfigurationDefaultsOverrides(): IStringDictionary<any> {
getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverride> {
return this.configurationDefaultsOverrides;
}
@ -526,15 +563,19 @@ class ConfigurationRegistry implements IConfigurationRegistry {
this._onDidSchemaChange.fire();
}
private updatePropertyDefaultValue(key: string, property: IConfigurationPropertySchema): void {
let defaultValue = this.configurationDefaultsOverrides[key];
private updatePropertyDefaultValue(key: string, property: IRegisteredConfigurationPropertySchema): void {
const configurationdefaultOverride = this.configurationDefaultsOverrides.get(key);
let defaultValue = configurationdefaultOverride?.value;
let defaultSource = configurationdefaultOverride?.source;
if (types.isUndefined(defaultValue)) {
defaultValue = property.default;
defaultValue = property.defaultDefaultValue;
defaultSource = undefined;
}
if (types.isUndefined(defaultValue)) {
defaultValue = getDefaultValue(property.type);
}
property.default = defaultValue;
property.defaultValueSource = defaultSource;
}
}

View file

@ -21,16 +21,16 @@ suite('ConfigurationRegistry', () => {
}
}
});
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 2, c: 3 } }]);
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 1, b: 2 } } }]);
configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 2, c: 3 } } }]);
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 1, b: 2 });
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, c: 3 });
});
test('configuration override defaults - merges defaults', async () => {
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ '[lang]': { a: 2, c: 3 } }]);
configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 1, b: 2 } } }]);
configurationRegistry.registerDefaultConfigurations([{ overrides: { '[lang]': { a: 2, c: 3 } } }]);
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 });
});
@ -45,8 +45,8 @@ suite('ConfigurationRegistry', () => {
}
}
});
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 1, b: 2 } }]);
configurationRegistry.registerDefaultConfigurations([{ 'config': { a: 2, c: 3 } }]);
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 1, b: 2 } } }]);
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 2, c: 3 } } }]);
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 });
});

View file

@ -58,6 +58,7 @@ export interface NativeParsedArgs {
'show-versions'?: boolean;
'category'?: string;
'install-extension'?: string[]; // undefined or array of 1 or more
'pre-release'?: boolean;
'install-builtin-extension'?: string[]; // undefined or array of 1 or more
'uninstall-extension'?: string[]; // undefined or array of 1 or more
'locate-extension'?: string[]; // undefined or array of 1 or more

View file

@ -56,6 +56,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") },
'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions."), args: 'category' },
'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") },
'pre-release': { type: 'boolean', cat: 'e', description: localize('install prerelease', "Installs the pre-release version of the extension, when using --install-extension") },
'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") },
'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") },

View file

@ -27,15 +27,14 @@ const MAX_SHELL_RESOLVE_TIME = 10000;
let unixShellEnvPromise: Promise<typeof process.env> | undefined = undefined;
/**
* We need to get the environment from a user's shell.
* This should only be done when Code itself is not launched
* from within a shell.
* Resolves the shell environment by spawning a shell. This call will cache
* the shell spawning so that subsequent invocations use that cached result.
*
* Will throw an error if:
* - we hit a timeout of `MAX_SHELL_RESOLVE_TIME`
* - any other error from spawning a shell to figure out the environment
*/
export async function resolveShellEnv(logService: ILogService, args: NativeParsedArgs, env: IProcessEnvironment): Promise<typeof process.env> {
export async function getResolvedShellEnv(logService: ILogService, args: NativeParsedArgs, env: IProcessEnvironment): Promise<typeof process.env> {
// Skip if --force-disable-user-env
if (args['force-disable-user-env']) {

View file

@ -14,7 +14,7 @@ import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import {
DidUninstallExtensionEvent, ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOperation, InstallOptions,
InstallVSIXOptions, IReportedExtension, StatisticType, UninstallOptions, TargetPlatform, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode
InstallVSIXOptions, IExtensionsControlManifest, StatisticType, UninstallOptions, TargetPlatform, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
@ -46,7 +46,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
declare readonly _serviceBrand: undefined;
private reportedExtensions: Promise<IReportedExtension[]> | undefined;
private extensionsControlManifest: Promise<IExtensionsControlManifest> | undefined;
private lastReportTimestamp = 0;
private readonly installingExtensions = new Map<string, IInstallExtensionTask>();
private readonly uninstallingExtensions = new Map<string, IUninstallExtensionTask>();
@ -120,15 +120,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
await this.installFromGallery(galleryExtension);
}
getExtensionsReport(): Promise<IReportedExtension[]> {
getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
const now = new Date().getTime();
if (!this.reportedExtensions || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness
this.reportedExtensions = this.updateReportCache();
if (!this.extensionsControlManifest || now - this.lastReportTimestamp > 1000 * 60 * 5) { // 5 minute cache freshness
this.extensionsControlManifest = this.updateControlCache();
this.lastReportTimestamp = now;
}
return this.reportedExtensions;
return this.extensionsControlManifest;
}
registerParticipant(participant: IExtensionManagementParticipant): void {
@ -353,10 +353,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
}
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, installPreRelease: boolean): Promise<{ extension: IGalleryExtension, manifest: IExtensionManifest }> {
if (await this.isMalicious(extension)) {
const report = await this.getExtensionsControlManifest();
if (getMaliciousExtensionsSet(report).has(extension.identifier.id)) {
throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious);
}
if (!!report.unsupportedPreReleaseExtensions && !!report.unsupportedPreReleaseExtensions[extension.identifier.id]) {
throw new ExtensionManagementError(nls.localize('unsupported prerelease extension', "Can't install '{0}' extension because it is no longer supported. It is now part of the '{1}' extension as a pre-release version.", extension.identifier.id, report.unsupportedPreReleaseExtensions[extension.identifier.id].displayName), ExtensionManagementErrorCode.UnsupportedPreRelease);
}
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);
@ -402,11 +407,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return compatibleExtension;
}
private async isMalicious(extension: IGalleryExtension): Promise<boolean> {
const report = await this.getExtensionsReport();
return getMaliciousExtensionsSet(report).has(extension.identifier.id);
}
private async unininstallExtension(extension: ILocalExtension, options: UninstallOptions): Promise<void> {
const uninstallExtensionTask = this.uninstallingExtensions.get(extension.identifier.id.toLowerCase());
if (uninstallExtensionTask) {
@ -579,15 +579,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return galleryResult.firstPage[0];
}
private async updateReportCache(): Promise<IReportedExtension[]> {
private async updateControlCache(): Promise<IExtensionsControlManifest> {
try {
this.logService.trace('ExtensionManagementService.refreshReportedCache');
const result = await this.galleryService.getExtensionsReport();
this.logService.trace(`ExtensionManagementService.refreshReportedCache - got ${result.length} reported extensions from service`);
return result;
const manifest = await this.galleryService.getExtensionsControlManifest();
this.logService.trace(`ExtensionManagementService.refreshControlCache`, manifest);
return manifest;
} catch (err) {
this.logService.trace('ExtensionManagementService.refreshReportedCache - failed to get extension report');
return [];
this.logService.trace('ExtensionManagementService.refreshControlCache - failed to get extension control manifest');
return { malicious: [] };
}
}

View file

@ -5,6 +5,7 @@
import { distinct } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStringDictionary } from 'vs/base/common/collections';
import { canceled, getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
import { getOrDefault } from 'vs/base/common/objects';
import { IPager } from 'vs/base/common/paging';
@ -15,7 +16,7 @@ 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';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { DefaultIconPath, getFallbackTargetPlarforms, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionIdentifierWithVersion, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IReportedExtension, isIExtensionIdentifier, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, TargetPlatform, toTargetPlatform, WEB_EXTENSION_TAG } from 'vs/platform/extensionManagement/common/extensionManagement';
import { DefaultIconPath, getFallbackTargetPlarforms, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionIdentifierWithVersion, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isIExtensionIdentifier, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, TargetPlatform, toTargetPlatform, WEB_EXTENSION_TAG } from 'vs/platform/extensionManagement/common/extensionManagement';
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
@ -438,9 +439,9 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
};
}
interface IRawExtensionsReport {
interface IRawExtensionsControlManifest {
malicious: string[];
slow: string[];
unsupported: IStringDictionary<boolean | { preReleaseExtension: { id: string, displayName: string } }>;
}
abstract class AbstractExtensionGalleryService implements IExtensionGalleryService {
@ -942,13 +943,13 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return engine;
}
async getExtensionsReport(): Promise<IReportedExtension[]> {
async getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
}
if (!this.extensionsControlUrl) {
return [];
return { malicious: [] };
}
const context = await this.requestService.request({ type: 'GET', url: this.extensionsControlUrl }, CancellationToken.None);
@ -956,18 +957,25 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
throw new Error('Could not get extensions report.');
}
const result = await asJson<IRawExtensionsReport>(context);
const map = new Map<string, IReportedExtension>();
const result = await asJson<IRawExtensionsControlManifest>(context);
const malicious: IExtensionIdentifier[] = [];
const unsupportedPreReleaseExtensions: IStringDictionary<{ id: string, displayName: string }> = {};
if (result) {
for (const id of result.malicious) {
const ext = map.get(id) || { id: { id }, malicious: true, slow: false };
ext.malicious = true;
map.set(id, ext);
malicious.push({ id });
}
if (result.unsupported) {
for (const extensionId of Object.keys(result.unsupported)) {
const value = result.unsupported[extensionId];
if (!isBoolean(value)) {
unsupportedPreReleaseExtensions[extensionId.toLowerCase()] = value.preReleaseExtension;
}
}
}
}
return [...map.values()];
return { malicious, unsupportedPreReleaseExtensions };
}
}

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStringDictionary } from 'vs/base/common/collections';
import { Event } from 'vs/base/common/event';
import { FileAccess } from 'vs/base/common/network';
import { IPager } from 'vs/base/common/paging';
@ -310,9 +311,9 @@ export const enum StatisticType {
Uninstall = 'uninstall'
}
export interface IReportedExtension {
id: IExtensionIdentifier;
malicious: boolean;
export interface IExtensionsControlManifest {
malicious: IExtensionIdentifier[];
unsupportedPreReleaseExtensions?: IStringDictionary<{ id: string, displayName: string }>;
}
export const enum InstallOperation {
@ -338,7 +339,7 @@ export interface IExtensionGalleryService {
getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null>;
getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise<ITranslation | null>;
getExtensionsReport(): Promise<IReportedExtension[]>;
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
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>;
@ -364,6 +365,7 @@ export interface DidUninstallExtensionEvent {
export enum ExtensionManagementErrorCode {
Unsupported = 'Unsupported',
UnsupportedPreRelease = 'UnsupportedPreRelease',
Malicious = 'Malicious',
Incompatible = 'Incompatible',
IncompatiblePreRelease = 'IncompatiblePreRelease',
@ -412,7 +414,7 @@ export interface IExtensionManagementService {
uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>;
reinstallFromGallery(extension: ILocalExtension): Promise<void>;
getInstalled(type?: ExtensionType, donotIgnoreInvalidExtensions?: boolean): Promise<ILocalExtension[]>;
getExtensionsReport(): Promise<IReportedExtension[]>;
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;
updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension>;
@ -483,7 +485,7 @@ export interface IExtensionManagementCLIService {
readonly _serviceBrand: undefined;
listExtensions(showVersions: boolean, category?: string, output?: CLIOutput): Promise<void>;
installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output?: CLIOutput): Promise<void>;
installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], installOptions: InstallOptions, force: boolean, output?: CLIOutput): Promise<void>;
uninstallExtensions(extensions: (string | URI)[], force: boolean, output?: CLIOutput): Promise<void>;
locateExtension(extensions: string[], output?: CLIOutput): Promise<void>;
}

View file

@ -89,7 +89,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
}
}
public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output: CLIOutput = console): Promise<void> {
public async installExtensions(extensions: (string | URI)[], builtinExtensionIds: string[], installOptions: InstallOptions, force: boolean, output: CLIOutput = console): Promise<void> {
const failed: string[] = [];
const installedExtensionsManifests: IExtensionManifest[] = [];
if (extensions.length) {
@ -119,21 +119,21 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
} else {
const [id, version] = getIdAndVersion(extension);
if (checkIfNotInstalled(id, version)) {
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } });
installExtensionInfos.push({ id, version, installOptions: { ...installOptions, isBuiltin: false } });
}
}
}
for (const extension of builtinExtensionIds) {
const [id, version] = getIdAndVersion(extension);
if (checkIfNotInstalled(id, version)) {
installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } });
installExtensionInfos.push({ id, version, installOptions: { ...installOptions, isBuiltin: true } });
}
}
if (vsixs.length) {
await Promise.all(vsixs.map(async vsix => {
try {
const manifest = await this.installVSIX(vsix, { isBuiltin: false, isMachineScoped }, force, output);
const manifest = await this.installVSIX(vsix, { ...installOptions, isBuiltin: false }, force, output);
if (manifest) {
installedExtensionsManifests.push(manifest);
}
@ -200,7 +200,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer
private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise<Map<string, IGalleryExtension>> {
const galleryExtensions = new Map<string, IGalleryExtension>();
const result = await this.extensionGalleryService.getExtensions(extensions, CancellationToken.None);
const result = await this.extensionGalleryService.getExtensions(extensions, extensions.some(e => e.installOptions.installPreReleaseVersion), CancellationToken.None);
for (const extension of result) {
galleryExtensions.set(extension.identifier.id.toLowerCase(), extension);
}

View file

@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects';
import { URI, UriComponents } from 'vs/base/common/uri';
import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IReportedExtension, isTargetPlatformCompatible, TargetPlatform, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IExtensionsControlManifest, isTargetPlatformCompatible, TargetPlatform, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI {
@ -72,7 +72,7 @@ export class ExtensionManagementChannel implements IServerChannel {
case 'getInstalled': return this.service.getInstalled(args[0]).then(extensions => extensions.map(e => transformOutgoingExtension(e, uriTransformer)));
case 'updateMetadata': return this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
case 'updateExtensionScope': return this.service.updateExtensionScope(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
case 'getExtensionsReport': return this.service.getExtensionsReport();
case 'getExtensionsControlManifest': return this.service.getExtensionsControlManifest();
}
throw new Error('Invalid call');
@ -169,8 +169,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
.then(extension => transformIncomingExtension(extension, null));
}
getExtensionsReport(): Promise<IReportedExtension[]> {
return Promise.resolve(this.channel.call('getExtensionsReport'));
getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
return Promise.resolve(this.channel.call('getExtensionsControlManifest'));
}
registerParticipant() { throw new Error('Not Supported'); }

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { compareIgnoreCase } from 'vs/base/common/strings';
import { IExtensionIdentifier, IExtensionIdentifierWithVersion, IGalleryExtension, ILocalExtension, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionIdentifier, IExtensionIdentifierWithVersion, IGalleryExtension, ILocalExtension, IExtensionsControlManifest } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionIdentifier, IExtension } from 'vs/platform/extensions/common/extensions';
export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifier): boolean {
@ -117,12 +117,12 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension):
export const BetterMergeId = new ExtensionIdentifier('pprice.better-merge');
export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set<string> {
export function getMaliciousExtensionsSet(manifest: IExtensionsControlManifest): Set<string> {
const result = new Set<string>();
for (const extension of report) {
if (extension.malicious) {
result.add(extension.id.id);
if (manifest.malicious) {
for (const extension of manifest.malicious) {
result.add(extension.id);
}
}

View file

@ -8,12 +8,12 @@ import { RunOnceScheduler } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ISocket, SocketCloseEvent, SocketCloseEventType } from 'vs/base/parts/ipc/common/ipc.net';
import { ISocket, SocketCloseEvent, SocketCloseEventType, SocketDiagnostics, SocketDiagnosticsEventType } from 'vs/base/parts/ipc/common/ipc.net';
import { IConnectCallback, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
export interface IWebSocketFactory {
create(url: string): IWebSocket;
create(url: string, debugLabel: string): IWebSocket;
}
export interface IWebSocketCloseEvent {
@ -41,6 +41,7 @@ export interface IWebSocket {
readonly onClose: Event<IWebSocketCloseEvent | void>;
readonly onError: Event<any>;
traceSocketEvent?(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void;
send(data: ArrayBuffer | ArrayBufferView): void;
close(): void;
}
@ -50,7 +51,8 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
private readonly _onData = new Emitter<ArrayBuffer>();
public readonly onData = this._onData.event;
public readonly onOpen: Event<void>;
private readonly _onOpen = this._register(new Emitter<void>());
public readonly onOpen = this._onOpen.event;
private readonly _onClose = this._register(new Emitter<IWebSocketCloseEvent>());
public readonly onClose = this._onClose.event;
@ -58,6 +60,7 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
private readonly _onError = this._register(new Emitter<any>());
public readonly onError = this._onError.event;
private readonly _debugLabel: string;
private readonly _socket: WebSocket;
private readonly _fileReader: FileReader;
private readonly _queue: Blob[];
@ -66,9 +69,15 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
private readonly _socketMessageListener: (ev: MessageEvent) => void;
constructor(socket: WebSocket) {
public traceSocketEvent(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void {
SocketDiagnostics.traceSocketEvent(this._socket, this._debugLabel, type, data);
}
constructor(url: string, debugLabel: string) {
super();
this._socket = socket;
this._debugLabel = debugLabel;
this._socket = new WebSocket(url);
this.traceSocketEvent(SocketDiagnosticsEventType.Created, { type: 'BrowserWebSocket', url });
this._fileReader = new FileReader();
this._queue = [];
this._isReading = false;
@ -78,6 +87,7 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
this._isReading = false;
const buff = <ArrayBuffer>(<any>event.target).result;
this.traceSocketEvent(SocketDiagnosticsEventType.Read, buff);
this._onData.fire(buff);
if (this._queue.length > 0) {
@ -95,11 +105,16 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
};
this._socketMessageListener = (ev: MessageEvent) => {
enqueue(<Blob>ev.data);
const blob = (<Blob>ev.data);
this.traceSocketEvent(SocketDiagnosticsEventType.BrowserWebSocketBlobReceived, { type: blob.type, size: blob.size });
enqueue(blob);
};
this._socket.addEventListener('message', this._socketMessageListener);
this.onOpen = Event.fromDOMEventEmitter(this._socket, 'open');
this._register(dom.addDisposableListener(this._socket, 'open', (e) => {
this.traceSocketEvent(SocketDiagnosticsEventType.Open);
this._onOpen.fire();
}));
// WebSockets emit error events that do not contain any real information
// Our only chance of getting to the root cause of an error is to
@ -134,6 +149,8 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
};
this._register(dom.addDisposableListener(this._socket, 'close', (e: CloseEvent) => {
this.traceSocketEvent(SocketDiagnosticsEventType.Close, { code: e.code, reason: e.reason, wasClean: e.wasClean });
this._isClosed = true;
if (pendingErrorEvent) {
@ -157,7 +174,10 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
this._onClose.fire({ code: e.code, reason: e.reason, wasClean: e.wasClean, event: e });
}));
this._register(dom.addDisposableListener(this._socket, 'error', sendErrorSoon));
this._register(dom.addDisposableListener(this._socket, 'error', (err) => {
this.traceSocketEvent(SocketDiagnosticsEventType.Error, { message: err?.message });
sendErrorSoon(err);
}));
}
send(data: ArrayBuffer | ArrayBufferView): void {
@ -165,11 +185,13 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
// Refuse to write data to closed WebSocket...
return;
}
this.traceSocketEvent(SocketDiagnosticsEventType.Write, data);
this._socket.send(data);
}
close(): void {
this._isClosed = true;
this.traceSocketEvent(SocketDiagnosticsEventType.Close);
this._socket.close();
this._socket.removeEventListener('message', this._socketMessageListener);
this.dispose();
@ -177,16 +199,27 @@ class BrowserWebSocket extends Disposable implements IWebSocket {
}
export const defaultWebSocketFactory = new class implements IWebSocketFactory {
create(url: string): IWebSocket {
return new BrowserWebSocket(new WebSocket(url));
create(url: string, debugLabel: string): IWebSocket {
return new BrowserWebSocket(url, debugLabel);
}
};
class BrowserSocket implements ISocket {
public readonly socket: IWebSocket;
constructor(socket: IWebSocket) {
public readonly socket: IWebSocket;
public readonly debugLabel: string;
public traceSocketEvent(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | any): void {
if (typeof this.socket.traceSocketEvent === 'function') {
this.socket.traceSocketEvent(type, data);
} else {
SocketDiagnostics.traceSocketEvent(this.socket, this.debugLabel, type, data);
}
}
constructor(socket: IWebSocket, debugLabel: string) {
this.socket = socket;
this.debugLabel = debugLabel;
}
public dispose(): void {
@ -239,13 +272,13 @@ export class BrowserSocketFactory implements ISocketFactory {
this._webSocketFactory = webSocketFactory || defaultWebSocketFactory;
}
connect(host: string, port: number, query: string, callback: IConnectCallback): void {
connect(host: string, port: number, query: string, debugLabel: string, callback: IConnectCallback): void {
const webSocketSchema = (/^https:/.test(window.location.href) ? 'wss' : 'ws');
const socket = this._webSocketFactory.create(`${webSocketSchema}://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=false`);
const socket = this._webSocketFactory.create(`${webSocketSchema}://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=false`, debugLabel);
const errorListener = socket.onError((err) => callback(err, undefined));
socket.onOpen(() => {
errorListener.dispose();
callback(undefined, new BrowserSocket(socket));
callback(undefined, new BrowserSocket(socket, debugLabel));
});
}
}

View file

@ -85,7 +85,7 @@ export interface IConnectCallback {
}
export interface ISocketFactory {
connect(host: string, port: number, query: string, callback: IConnectCallback): void;
connect(host: string, port: number, query: string, debugLabel: string, callback: IConnectCallback): void;
}
function createTimeoutCancellation(millis: number): CancellationToken {
@ -188,9 +188,9 @@ function readOneControlMessage<T>(protocol: PersistentProtocol, timeoutCancellat
return result.promise;
}
function createSocket(logService: ILogService, socketFactory: ISocketFactory, host: string, port: number, query: string, timeoutCancellationToken: CancellationToken): Promise<ISocket> {
function createSocket(logService: ILogService, socketFactory: ISocketFactory, host: string, port: number, query: string, debugLabel: string, timeoutCancellationToken: CancellationToken): Promise<ISocket> {
const result = new PromiseWithTimeout<ISocket>(timeoutCancellationToken);
socketFactory.connect(host, port, query, (err: any, socket: ISocket | undefined) => {
socketFactory.connect(host, port, query, debugLabel, (err: any, socket: ISocket | undefined) => {
if (result.didTimeout) {
if (err) {
logService.error(err);
@ -231,7 +231,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio
let socket: ISocket;
try {
socket = await createSocket(options.logService, options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, timeoutCancellationToken);
socket = await createSocket(options.logService, options.socketFactory, options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, `renderer-${connectionTypeToString(connectionType)}-${options.reconnectionToken}`, timeoutCancellationToken);
} catch (error) {
options.logService.error(`${logPrefix} socketFactory.connect() failed or timed out. Error:`);
options.logService.error(error);
@ -512,7 +512,7 @@ export class ReconnectionPermanentFailureEvent {
}
export type PersistentConnectionEvent = ConnectionGainEvent | ConnectionLostEvent | ReconnectionWaitEvent | ReconnectionRunningEvent | ReconnectionPermanentFailureEvent;
abstract class PersistentConnection extends Disposable {
export abstract class PersistentConnection extends Disposable {
public static triggerPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void {
this._permanentFailure = true;
@ -521,6 +521,15 @@ abstract class PersistentConnection extends Disposable {
this._permanentFailureHandled = handled;
this._instances.forEach(instance => instance._gotoPermanentFailure(this._permanentFailureMillisSinceLastIncomingData, this._permanentFailureAttempt, this._permanentFailureHandled));
}
public static debugTriggerReconnection() {
this._instances.forEach(instance => instance._beginReconnecting());
}
public static debugPauseSocketWriting() {
this._instances.forEach(instance => instance._pauseSocketWriting());
}
private static _permanentFailure: boolean = false;
private static _permanentFailureMillisSinceLastIncomingData: number = 0;
private static _permanentFailureAttempt: number = 0;
@ -678,6 +687,10 @@ abstract class PersistentConnection extends Disposable {
safeDisposeProtocolAndSocket(this.protocol);
}
private _pauseSocketWriting(): void {
this.protocol.pauseSocketWriting();
}
protected abstract _reconnect(options: ISimpleConnectionOptions, timeoutCancellationToken: CancellationToken): Promise<void>;
}

View file

@ -8,7 +8,7 @@ import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { IConnectCallback, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
export const nodeSocketFactory = new class implements ISocketFactory {
connect(host: string, port: number, query: string, callback: IConnectCallback): void {
connect(host: string, port: number, query: string, debugLabel: string, callback: IConnectCallback): void {
const errorListener = (err: any) => callback(err, undefined);
const socket = net.createConnection({ host: host, port: port }, () => {
@ -34,7 +34,7 @@ export const nodeSocketFactory = new class implements ISocketFactory {
if (strData.indexOf('\r\n\r\n') >= 0) {
// headers received OK
socket.off('data', onData);
callback(undefined, new NodeSocket(socket));
callback(undefined, new NodeSocket(socket, debugLabel));
}
};
socket.on('data', onData);

View file

@ -16,7 +16,7 @@ import { isBoolean, isNumber } from 'vs/base/common/types';
import { IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
import { getResolvedShellEnv } from 'vs/platform/environment/node/shellEnv';
import { ILogService } from 'vs/platform/log/common/log';
import { IHTTPConfiguration, IRequestService } from 'vs/platform/request/common/request';
import { Agent, getProxyAgent } from 'vs/platform/request/node/proxy';
@ -67,7 +67,7 @@ export class RequestService extends Disposable implements IRequestService {
let shellEnv: typeof process.env | undefined = undefined;
try {
shellEnv = await resolveShellEnv(this.logService, this.environmentService.args, process.env);
shellEnv = await getResolvedShellEnv(this.logService, this.environmentService.args, process.env);
} catch (error) {
this.logService.error('RequestService#request resolving shell environment failed', error);
}

View file

@ -12,7 +12,7 @@ import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { parsePtyHostPort } from 'vs/platform/environment/common/environmentService';
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
import { getResolvedShellEnv } from 'vs/platform/environment/node/shellEnv';
import { ILogService } from 'vs/platform/log/common/log';
import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -124,7 +124,7 @@ export class PtyHostService extends Disposable implements IPtyService {
}
try {
return await resolveShellEnv(this._logService, { _: [] }, process.env);
return await getResolvedShellEnv(this._logService, { _: [] }, process.env);
} catch (error) {
this._logService.error('ptyHost was unable to resolve shell environment', error);

View file

@ -416,7 +416,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
) {
if (await this.extensionManagementService.canInstall(extension)) {
this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...`, e.identifier.id, extension.version);
await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: e.preRelease } /* pass options to prevent install and sync dialog in web */);
await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: e.preRelease } /* set isMachineScoped value to prevent install and sync dialog in web */);
this.logService.info(`${this.syncResourceLogLabel}: Installed extension.`, e.identifier.id, extension.version);
removeFromSkipped.push(extension.identifier);
} else {

View file

@ -13,7 +13,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { IRemoteConsoleLog } from 'vs/base/common/console';
import { Emitter, Event } from 'vs/base/common/event';
import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
import { getResolvedShellEnv } from 'vs/platform/environment/node/shellEnv';
import { ILogService } from 'vs/platform/log/common/log';
import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection';
import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
@ -27,7 +27,7 @@ export async function buildUserEnvironment(startParamsEnv: { [key: string]: stri
let userShellEnv: typeof process.env | undefined = undefined;
try {
userShellEnv = await resolveShellEnv(logService, environmentService.args, process.env);
userShellEnv = await getResolvedShellEnv(logService, environmentService.args, process.env);
} catch (error) {
logService.error('ExtensionHostConnection#buildUserEnvironment resolving shell environment failed', error);
userShellEnv = {};
@ -110,7 +110,6 @@ export class ExtensionHostConnection {
this._remoteAddress = remoteAddress;
this._extensionHostProcess = null;
this._connectionData = ExtensionHostConnection._toConnectionData(socket, initialDataChunk);
this._connectionData.socket.pause();
this._log(`New connection established.`);
}
@ -156,7 +155,6 @@ export class ExtensionHostConnection {
this._remoteAddress = remoteAddress;
this._log(`The client has reconnected.`);
const connectionData = ExtensionHostConnection._toConnectionData(_socket, initialDataChunk);
connectionData.socket.pause();
if (!this._extensionHostProcess) {
// The extension host didn't even start up yet

View file

@ -27,7 +27,7 @@ import { ProcessItem } from 'vs/base/common/processes';
import { ILog, Translations } from 'vs/workbench/services/extensions/common/extensionPoints';
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { IBuiltInExtension } from 'vs/base/common/product';
import { IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementCLIService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { cwd } from 'vs/base/common/process';
import { IRemoteTelemetryService } from 'vs/server/remoteTelemetryService';
import { Promises } from 'vs/base/node/pfs';
@ -77,7 +77,8 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
};
if (environmentService.args['install-builtin-extension']) {
this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], environmentService.args['install-builtin-extension'], !!environmentService.args['do-not-sync'], !!environmentService.args['force'])
const installOptions: InstallOptions = { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] };
this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], environmentService.args['install-builtin-extension'], installOptions, !!environmentService.args['force'])
.then(null, error => {
logService.error(error);
});
@ -89,7 +90,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
if (extensionsToInstall) {
const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input);
this.whenExtensionsReady
.then(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], !!environmentService.args['do-not-sync'], !!environmentService.args['force']))
.then(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }, !!environmentService.args['force']))
.then(null, error => {
logService.error(error);
});

View file

@ -12,7 +12,7 @@ import { IRequestService } from 'vs/platform/request/common/request';
import { RequestService } from 'vs/platform/request/node/requestService';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
@ -114,7 +114,8 @@ class CliMain extends Disposable {
// Install Extension
else if (this.args['install-extension'] || this.args['install-builtin-extension']) {
return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.args['install-extension'] || []), this.args['install-builtin-extension'] || [], !!this.args['do-not-sync'], !!this.args['force']);
const installOptions: InstallOptions = { isMachineScoped: !!this.args['do-not-sync'], installPreReleaseVersion: !!this.args['pre-release'] };
return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.args['install-extension'] || []), this.args['install-builtin-extension'] || [], installOptions, !!this.args['force']);
}
// Uninstall Extension

View file

@ -497,9 +497,9 @@ export class RemoteExtensionHostAgentServer extends Disposable {
// Finally!
if (skipWebSocketFrames) {
this._handleWebSocketConnection(new NodeSocket(socket), isReconnection, reconnectionToken);
this._handleWebSocketConnection(new NodeSocket(socket, `server-connection-${reconnectionToken}`), isReconnection, reconnectionToken);
} else {
this._handleWebSocketConnection(new WebSocketNodeSocket(new NodeSocket(socket), permessageDeflate, null, true), isReconnection, reconnectionToken);
this._handleWebSocketConnection(new WebSocketNodeSocket(new NodeSocket(socket, `server-connection-${reconnectionToken}`), permessageDeflate, null, true), isReconnection, reconnectionToken);
}
}
@ -754,6 +754,7 @@ export class RemoteExtensionHostAgentServer extends Disposable {
}
}
protocol.sendPause();
protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {})));
const dataChunk = protocol.readEntireBuffer();
protocol.dispose();
@ -766,6 +767,7 @@ export class RemoteExtensionHostAgentServer extends Disposable {
return this._rejectWebSocketConnection(logPrefix, protocol, `Duplicate reconnection token`);
}
protocol.sendPause();
protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {})));
const dataChunk = protocol.readEntireBuffer();
protocol.dispose();

View file

@ -120,6 +120,7 @@ export interface ServerParsedArgs {
force?: boolean; // used by install-extension
'do-not-sync'?: boolean; // used by install-extension
'pre-release'?: boolean; // used by install-extension
'user-data-dir'?: string;
'builtin-extensions-dir'?: string;

View file

@ -71,7 +71,7 @@ CommandsRegistry.registerCommand('_remoteCLI.manageExtensions', async function (
const revive = (inputs: (string | UriComponents)[]) => inputs.map(input => isString(input) ? input : URI.revive(input));
if (Array.isArray(args.install) && args.install.length) {
try {
await cliService.installExtensions(revive(args.install), [], true, !!args.force, output);
await cliService.installExtensions(revive(args.install), [], { isMachineScoped: true }, !!args.force, output);
} catch (e) {
lines.push(e.message);
}

View file

@ -204,12 +204,12 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
switch (source) {
case CandidatePortSource.None: {
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
.registerDefaultConfigurations([{ 'remote.autoForwardPorts': false }]);
.registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPorts': false } }]);
break;
}
case CandidatePortSource.Output: {
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
.registerDefaultConfigurations([{ 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT }]);
.registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]);
break;
}
default: // Do nothing, the defaults for these settings should be used.

View file

@ -8,7 +8,7 @@ import * as objects from 'vs/base/common/objects';
import { Registry } from 'vs/platform/registry/common/platform';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IConfigurationNode, IConfigurationRegistry, Extensions, resourceLanguageSettingsSchemaId, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN, OVERRIDE_PROPERTY_REGEX, windowSettings, resourceSettings, machineOverridableSettings } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationNode, IConfigurationRegistry, Extensions, resourceLanguageSettingsSchemaId, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN, OVERRIDE_PROPERTY_REGEX, windowSettings, resourceSettings, machineOverridableSettings, IConfigurationDefaults } from 'vs/platform/configuration/common/configurationRegistry';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { workspaceSettingsSchemaId, launchSchemaId, tasksSchemaId } from 'vs/workbench/services/configuration/common/configuration';
import { isObject } from 'vs/base/common/types';
@ -144,24 +144,24 @@ const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<I
});
defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => {
if (removed.length) {
const removedDefaultConfigurations = removed.map<IStringDictionary<any>>(extension => objects.deepClone(extension.value));
const removedDefaultConfigurations = removed.map<IConfigurationDefaults>(extension => ({ overrides: objects.deepClone(extension.value), source: { id: extension.description.identifier.value, displayName: extension.description.displayName } }));
configurationRegistry.deregisterDefaultConfigurations(removedDefaultConfigurations);
}
if (added.length) {
const registeredProperties = configurationRegistry.getConfigurationProperties();
const allowedScopes = [ConfigurationScope.MACHINE_OVERRIDABLE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE];
const addedDefaultConfigurations = added.map<IStringDictionary<any>>(extension => {
const defaults: IStringDictionary<any> = objects.deepClone(extension.value);
for (const key of Object.keys(defaults)) {
const addedDefaultConfigurations = added.map<IConfigurationDefaults>(extension => {
const overrides: IStringDictionary<any> = objects.deepClone(extension.value);
for (const key of Object.keys(overrides)) {
if (!OVERRIDE_PROPERTY_REGEX.test(key)) {
const registeredPropertyScheme = registeredProperties[key];
if (registeredPropertyScheme.scope && !allowedScopes.includes(registeredPropertyScheme.scope)) {
extension.collector.warn(nls.localize('config.property.defaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. Only defaults for machine-overridable, window, resource and language overridable scoped settings are supported.", key));
delete defaults[key];
delete overrides[key];
}
}
}
return defaults;
return { overrides, source: { id: extension.description.identifier.value, displayName: extension.description.displayName } };
});
configurationRegistry.registerDefaultConfigurations(addedDefaultConfigurations);
}
@ -212,7 +212,8 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => {
validateProperties(configuration, extension);
configuration.id = node.id || extension.description.identifier.value;
configuration.extensionInfo = { id: extension.description.identifier.value, restrictedConfigurations: extension.description.capabilities?.untrustedWorkspaces?.supported === 'limited' ? extension.description.capabilities?.untrustedWorkspaces.restrictedConfigurations : undefined };
configuration.extensionInfo = { id: extension.description.identifier.value, displayName: extension.description.displayName };
configuration.restrictedProperties = extension.description.capabilities?.untrustedWorkspaces?.supported === 'limited' ? extension.description.capabilities?.untrustedWorkspaces.restrictedConfigurations : undefined;
configuration.title = configuration.title || extension.description.displayName || extension.description.identifier.value;
configurations.push(configuration);
return configurations;

View file

@ -98,6 +98,7 @@ export class ExtHostLanguages implements ExtHostLanguagesShape {
command: undefined,
text: '',
detail: '',
busy: false
};
let soonHandle: IDisposable | undefined;
@ -115,7 +116,8 @@ export class ExtHostLanguages implements ExtHostLanguagesShape {
detail: data.detail ?? '',
severity: data.severity === LanguageStatusSeverity.Error ? Severity.Error : data.severity === LanguageStatusSeverity.Warning ? Severity.Warning : Severity.Info,
command: data.command && this._commands.toInternal(data.command, commandDisposables),
accessibilityInfo: data.accessibilityInformation
accessibilityInfo: data.accessibilityInformation,
busy: data.busy
});
}, 0);
};
@ -178,6 +180,13 @@ export class ExtHostLanguages implements ExtHostLanguagesShape {
set command(value) {
data.command = value;
updateAsync();
},
get busy() {
return data.busy;
},
set busy(value: boolean) {
data.busy = value;
updateAsync();
}
};
updateAsync();

View file

@ -23,14 +23,14 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
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, PreReleaseIndicatorWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget, PreReleaseIndicatorWidget, ExtensionHoverWidget } 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, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction
InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, SwitchUnsupportedExtensionToPreReleaseExtensionAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
@ -68,7 +68,7 @@ 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 { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { errorIcon, infoIcon, starEmptyIcon, verifiedPublisherIcon as verifiedPublisherThemeIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { errorIcon, infoIcon, preReleaseIcon, starEmptyIcon, verifiedPublisherIcon as verifiedPublisherThemeIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { ViewContainerLocation } from 'vs/workbench/common/views';
@ -150,6 +150,7 @@ interface IExtensionEditorTemplate {
actionsAndStatusContainer: HTMLElement;
extensionActionBar: ActionBar;
status: HTMLElement;
preReleaseText: HTMLElement;
recommendation: HTMLElement;
navbar: NavBar;
content: HTMLElement;
@ -271,6 +272,7 @@ export class ExtensionEditor extends EditorPane {
}));
const status = append(actionsAndStatusContainer, $('.status'));
const preReleaseText = append(details, $('.pre-release-text'));
const recommendation = append(details, $('.recommendation'));
this._register(Event.chain(extensionActionBar.onDidRun)
@ -303,6 +305,7 @@ export class ExtensionEditor extends EditorPane {
rating,
actionsAndStatusContainer,
extensionActionBar,
preReleaseText,
status,
recommendation
};
@ -456,6 +459,7 @@ export class ExtensionEditor extends EditorPane {
]),
this.instantiationService.createInstance(SwitchToPreReleaseVersionAction),
this.instantiationService.createInstance(SwitchToReleasedVersionAction),
this.instantiationService.createInstance(SwitchUnsupportedExtensionToPreReleaseExtensionAction),
this.instantiationService.createInstance(ToggleSyncExtensionAction),
new ExtensionEditorManageExtensionAction(this.scopedContextKeyService || this.contextKeyService, this.instantiationService),
];
@ -475,6 +479,7 @@ export class ExtensionEditor extends EditorPane {
this.transientDisposables.add(disposable);
}
this.setPreReleaseText(extension, template);
this.setStatus(extension, extensionStatus, template);
this.setRecommendationText(extension, template);
@ -523,6 +528,30 @@ export class ExtensionEditor extends EditorPane {
this.editorLoadComplete = true;
}
private setPreReleaseText(extension: IExtension, template: IExtensionEditorTemplate): void {
let preReleaseText: string | undefined;
reset(template.preReleaseText);
const disposables = this.transientDisposables.add(new DisposableStore());
const updatePreReleaseText = () => {
const newPreReleaseText = ExtensionHoverWidget.getPreReleaseMessage(extension);
if (preReleaseText !== newPreReleaseText) {
preReleaseText = newPreReleaseText;
disposables.clear();
reset(template.preReleaseText);
if (preReleaseText) {
append(template.preReleaseText, $(`span${ThemeIcon.asCSSSelector(preReleaseIcon)}`));
disposables.add(this.renderMarkdownText(preReleaseText, template.preReleaseText));
}
}
};
updatePreReleaseText();
this.transientDisposables.add(this.extensionsWorkbenchService.onChange(e => {
if (e && areSameExtensions(e.identifier, extension.identifier)) {
updatePreReleaseText();
}
}));
}
private setStatus(extension: IExtension, extensionStatus: ExtensionStatusAction, template: IExtensionEditorTemplate): void {
const disposables = new DisposableStore();
this.transientDisposables.add(disposables);
@ -535,16 +564,7 @@ export class ExtensionEditor extends EditorPane {
const statusIconActionBar = disposables.add(new ActionBar(template.status, { animated: false }));
statusIconActionBar.push(extensionStatus, { icon: true, label: false });
}
const rendered = disposables.add(renderMarkdown(new MarkdownString(status.message.value, { isTrusted: true, supportThemeIcons: true }), {
actionHandler: {
callback: (content) => {
this.openerService.open(content, { allowCommands: true }).catch(onUnexpectedError);
},
disposables: disposables
}
}));
append(append(template.status, $('.status-text')),
rendered.element);
disposables.add(this.renderMarkdownText(status.message.value, append(template.status, $('.status-text'))));
}
};
updateStatus();
@ -573,6 +593,20 @@ export class ExtensionEditor extends EditorPane {
this.transientDisposables.add(this.extensionRecommendationsService.onDidChangeRecommendations(() => updateRecommendationText()));
}
private renderMarkdownText(markdownText: string, parent: HTMLElement): IDisposable {
const disposables = new DisposableStore();
const rendered = disposables.add(renderMarkdown(new MarkdownString(markdownText, { isTrusted: true, supportThemeIcons: true }), {
actionHandler: {
callback: (content) => {
this.openerService.open(content, { allowCommands: true }).catch(onUnexpectedError);
},
disposables: disposables
}
}));
append(parent, rendered.element);
return disposables;
}
override clearInput(): void {
this.contentDisposables.clear();
this.transientDisposables.clear();
@ -1768,7 +1802,6 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
if (link) {
collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a { color: ${link}; }`);
collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a { color: ${link}; }`);
collector.addRule(`.monaco-workbench .extension-editor > .header > .details > .actions-status-container > .status > .status-text a { color: ${link}; }`);
}
const activeLink = theme.getColor(textLinkActiveForeground);
@ -1777,9 +1810,6 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a:active { color: ${activeLink}; }`);
collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a:hover,
.monaco-workbench .extension-editor .content .feature-contributions a:active { color: ${activeLink}; }`);
collector.addRule(`.monaco-workbench .extension-editor > .header > .details > .actions-status-container > .status > .status-text a:hover,
.monaco-workbench .extension-editor > .header > .details > actions-status-container > .status > .status-text a:active { color: ${activeLink}; }`);
}
const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground);

View file

@ -8,7 +8,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { Registry } from 'vs/platform/registry/common/platform';
import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
@ -74,6 +74,7 @@ import { ExtensionsCompletionItemsProvider } from 'vs/workbench/contrib/extensio
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { Event } from 'vs/base/common/event';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { UnsupportedPreReleaseExtensionsChecker } from 'vs/workbench/contrib/extensions/browser/unsupportedPreReleaseExtensionsChecker';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
@ -274,6 +275,11 @@ CommandsRegistry.registerCommand({
'description': localize('workbench.extensions.installExtension.option.installOnlyNewlyAddedFromExtensionPackVSIX', "When enabled, VS Code installs only newly added extensions from the extension pack VSIX. This option is considered only while installing a VSIX."),
default: false
},
'installPreReleaseVersion': {
'type': 'boolean',
'description': localize('workbench.extensions.installExtension.option.installPreReleaseVersion', "When enabled, VS Code installs the pre-release version of the extension if available."),
default: false
},
'donotSync': {
'type': 'boolean',
'description': localize('workbench.extensions.installExtension.option.donotSync', "When enabled, VS Code do not sync this extension when Settings Sync is on."),
@ -284,14 +290,18 @@ CommandsRegistry.registerCommand({
}
]
},
handler: async (accessor, arg: string | UriComponents, options?: { installOnlyNewlyAddedFromExtensionPackVSIX?: boolean, donotSync?: boolean }) => {
handler: async (accessor, arg: string | UriComponents, options?: { installOnlyNewlyAddedFromExtensionPackVSIX?: boolean, installPreReleaseVersion?: boolean, donotSync?: boolean }) => {
const extensionManagementService = accessor.get(IExtensionManagementService);
const extensionGalleryService = accessor.get(IExtensionGalleryService);
try {
if (typeof arg === 'string') {
const [extension] = await extensionGalleryService.getExtensions([{ id: arg }], CancellationToken.None);
if (extension) {
await extensionManagementService.installFromGallery(extension, options?.donotSync ? { isMachineScoped: true } : undefined);
const installOptions: InstallOptions = {
isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */
installPreReleaseVersion: options?.installPreReleaseVersion
};
await extensionManagementService.installFromGallery(extension, installOptions);
} else {
throw new Error(localize('notFound', "Extension '{0}' not found.", arg));
}
@ -1158,7 +1168,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
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' },
title: { value: localize('show pre-release version', "Show Pre-Release Version"), original: 'Show Pre-Release Version' },
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
@ -1173,7 +1183,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
});
this.registerExtensionAction({
id: 'workbench.extensions.action.showReleasedVersion',
title: { value: localize('show released version', "Show Released Version"), original: 'Show Released Version' },
title: { value: localize('show released version', "Show Release Version"), original: 'Show Release Version' },
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
@ -1452,6 +1462,7 @@ const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Workbench
workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(MaliciousExtensionChecker, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(UnsupportedPreReleaseExtensionsChecker, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually);

View file

@ -14,7 +14,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { dispose } from 'vs/base/common/lifecycle';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate';
import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
@ -64,6 +64,7 @@ import { escapeMarkdownSyntaxTokens, IMarkdownString, MarkdownString } from 'vs/
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { ViewContainerLocation } from 'vs/workbench/common/views';
import { flatten } from 'vs/base/common/arrays';
import { isBoolean } from 'vs/base/common/types';
function getRelativeDateLabel(date: Date): string {
const delta = new Date().getTime() - date.getTime();
@ -136,7 +137,7 @@ export class PromptExtensionInstallFailureAction extends Action {
return;
}
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious].includes(<ExtensionManagementErrorCode>this.error.name)) {
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.UnsupportedPreRelease].includes(<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.show(Severity.Info, getErrorMessage(this.error));
return;
}
@ -148,9 +149,9 @@ export class PromptExtensionInstallFailureAction extends Action {
if (ExtensionManagementErrorCode.IncompatiblePreRelease === (<ExtensionManagementErrorCode>this.error.name)) {
operationMessage = getErrorMessage(this.error);
additionalMessage = localize('install release version', "Would you like to install the released version?");
additionalMessage = localize('install release version message', "Would you like to install the release version?");
promptChoices.push({
label: localize('install released version', "Install Released Version"),
label: localize('install release version', "Install Release Version"),
run: () => {
const installAction = this.installOptions?.isMachineScoped ? this.instantiationService.createInstance(InstallAction, !!this.installOptions.installPreReleaseVersion) : this.instantiationService.createInstance(InstallAndSyncAction, !!this.installOptions?.installPreReleaseVersion);
installAction.extension = this.extension;
@ -367,11 +368,11 @@ export abstract class AbstractInstallAction extends ExtensionAction {
getLabel(primary?: boolean): string {
/* install pre-release version */
if (this.installPreReleaseVersion && this.extension?.hasPreReleaseVersion) {
return primary ? localize('install pre-release', "Install Pre-release") : localize('install pre-release version', "Install Pre-release Version");
return primary ? localize('install pre-release', "Install Pre-Release") : localize('install pre-release version', "Install Pre-Release Version");
}
/* install released version that has a pre release version */
if (this.extension?.hasPreReleaseVersion) {
return primary ? localize('install', "Install") : localize('install released version', "Install Released Version");
return primary ? localize('install', "Install") : localize('install release version', "Install Release Version");
}
return localize('install', "Install");
}
@ -1076,7 +1077,7 @@ export class MenuItemExtensionAction extends ExtensionAction {
export class SwitchToPreReleaseVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.switchToPreReleaseVersion';
static readonly TITLE = { value: localize('switch to pre-release version', "Switch to Pre-release Version"), original: 'Switch to Pre-release Version' };
static readonly TITLE = { value: localize('switch to pre-release version', "Switch to Pre-Release Version"), original: 'Switch to Pre-Release Version' };
private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} hide-when-disabled`;
@ -1101,8 +1102,8 @@ export class SwitchToPreReleaseVersionAction extends ExtensionAction {
export class SwitchToReleasedVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.switchToReleasedVersion';
static readonly TITLE = { value: localize('switch to released version', "Switch to Released Version"), original: 'Switch to Released Version' };
static readonly ID = 'workbench.extensions.action.switchToReleaseVersion';
static readonly TITLE = { value: localize('switch to release version', "Switch to Release Version"), original: 'Switch to Release Version' };
private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} hide-when-disabled`;
@ -1125,6 +1126,68 @@ export class SwitchToReleasedVersionAction extends ExtensionAction {
}
}
export class SwitchUnsupportedExtensionToPreReleaseExtensionAction extends ExtensionAction {
private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} hide-when-disabled`;
constructor(
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super('workbench.extensions.action.switchUnsupportedExtensionToPreReleaseExtension', '', SwitchUnsupportedExtensionToPreReleaseExtensionAction.Class);
}
update(): void {
this.enabled = false;
if (!!this.extension && !!this.extension.local && this.extension.isUnsupported && !isBoolean(this.extension.isUnsupported) && this.extension.state === ExtensionState.Installed) {
this.enabled = true;
this.label = localize('switchUnsupportedExtensionToPreReleaseExtension', "Switch to '{0}'", this.extension.isUnsupported.preReleaseExtension.displayName);
}
}
override async run(): Promise<any> {
if (!!this.extension && !!this.extension.local && this.extension.isUnsupported && !isBoolean(this.extension.isUnsupported)) {
const gallery = (await this.galleryService.getExtensions([{ id: this.extension.isUnsupported.preReleaseExtension.id }], true, CancellationToken.None))[0];
return this.instantiationService.createInstance(SwitchUnsupportedExtensionToPreReleaseExtensionCommandAction, this.extension.local, gallery, false).run();
}
}
}
export class SwitchUnsupportedExtensionToPreReleaseExtensionCommandAction extends Action {
constructor(
private readonly local: ILocalExtension,
private readonly gallery: IGalleryExtension,
private promptReload: boolean,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IProductService private readonly productService: IProductService,
@IHostService private readonly hostService: IHostService,
@IWorkbenchExtensionEnablementService private readonly workbenchExtensionEnablementService: IWorkbenchExtensionEnablementService,
@INotificationService private readonly notificationService: INotificationService,
) {
super('workbench.extensions.action.switchUnsupportedExtensionToPreReleaseExtensionCommand', localize('switchUnsupportedExtensionToPreReleaseExtension', "Switch to '{0}'", gallery.displayName));
}
override async run(): Promise<any> {
await Promise.all([
this.extensionManagementService.uninstall(this.local),
this.extensionManagementService.installFromGallery(this.gallery, { installPreReleaseVersion: true, isMachineScoped: this.local.isMachineScoped })
.then(local => this.workbenchExtensionEnablementService.setEnablement([this.local], EnablementState.EnabledGlobally)),
]);
if (this.promptReload) {
this.notificationService.prompt(
Severity.Info,
localize('SwitchToAnotherReleaseExtension.successReload', "Please reload {0} to complete switching to the '{1}' extension.", this.productService.nameLong, this.gallery.displayName),
[{
label: localize('reloadNow', "Reload Now"),
run: () => this.hostService.reload()
}],
{ sticky: true }
);
}
}
}
export class InstallAnotherVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.install.anotherVersion';
@ -1175,7 +1238,7 @@ export class InstallAnotherVersionAction extends ExtensionAction {
label: v.version,
description: `${getRelativeDateLabel(new Date(Date.parse(v.date)))}${v.isPreReleaseVersion ? ` (${localize('pre-release', "pre-release")})` : ''}${v.version === this.extension!.version ? ` (${localize('current', "current")})` : ''}`,
latest: i === 0,
ariaLabel: `${v.isPreReleaseVersion ? 'Pre-release version' : 'Released version'} ${v.version}`,
ariaLabel: `${v.isPreReleaseVersion ? 'Pre-Release version' : 'Release version'} ${v.version}`,
isPreReleaseVersion: v.isPreReleaseVersion
};
});
@ -2177,12 +2240,21 @@ export class ExtensionStatusAction extends ExtensionAction {
return;
}
if (this.extension.gallery && this.extension.state === ExtensionState.Uninstalled && !await this.extensionsWorkbenchService.canInstall(this.extension)) {
if (this.extension.isMalicious) {
this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('malicious tooltip', "This extension was reported to be problematic.")) }, true);
return;
if (this.extension.isMalicious) {
this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('malicious tooltip', "This extension was reported to be problematic.")) }, true);
return;
}
if (this.extension.isUnsupported) {
if (isBoolean(this.extension.isUnsupported)) {
this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('unsupported tooltip', "This extension no longer supported.")) }, true);
} else {
const link = `[${this.extension.isUnsupported.preReleaseExtension.displayName}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.isUnsupported.preReleaseExtension.id]))}`)})`;
this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('unsupported prerelease tooltip', "This extension is no longer supported and is now part of the {0} extension as a pre-release version. We recommend that you switch to it.", link)) }, true);
}
return;
}
if (this.extension.gallery && this.extension.state === ExtensionState.Uninstalled && !await this.extensionsWorkbenchService.canInstall(this.extension)) {
if (this.extensionManagementServerService.localExtensionManagementServer || this.extensionManagementServerService.remoteExtensionManagementServer) {
const targetPlatform = await (this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.localExtensionManagementServer!.extensionManagementService.getTargetPlatform() : this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getTargetPlatform());
const message = new MarkdownString(`${localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", this.extension.displayName || this.extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform))} [${localize('learn more', "Learn More")}](https://aka.ms/vscode-platform-specific-extensions)`);

View file

@ -825,7 +825,7 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution {
}
private checkForMaliciousExtensions(): Promise<void> {
return this.extensionsManagementService.getExtensionsReport().then(report => {
return this.extensionsManagementService.getExtensionsControlManifest().then(report => {
const maliciousSet = getMaliciousExtensionsSet(report);
return this.extensionsManagementService.getInstalled(ExtensionType.User).then(installed => {

View file

@ -176,7 +176,7 @@ export class PreReleaseIndicatorWidget extends ExtensionWidget {
return;
}
if (!this.extension.local?.isPreReleaseVersion) {
if (!this.extension.local?.isPreReleaseVersion && !this.extension.gallery?.properties.isPreReleaseVersion) {
return;
}
@ -184,7 +184,7 @@ export class PreReleaseIndicatorWidget extends ExtensionWidget {
append(this.container, $('span' + ThemeIcon.asCSSSelector(preReleaseIcon)));
}
if (this.options?.label) {
append(this.container, $('span.pre-releaselabel', undefined, localize('pre-release-label', "Pre-release")));
append(this.container, $('span.pre-releaselabel', undefined, localize('pre-release-label', "Pre-Release")));
}
}
}
@ -482,9 +482,9 @@ 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) {
if (this.extension.local?.isPreReleaseVersion || this.extension.gallery?.properties.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.appendMarkdown(`&nbsp;<span style="color:#ffffff;background-color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">&nbsp;${localize('pre-release-label', "Pre-Release")}&nbsp;</span>`);
}
markdown.appendText(`\n`);
@ -500,18 +500,13 @@ export class ExtensionHoverWidget extends ExtensionWidget {
markdown.appendText(`\n`);
}
const preReleaseMessage = this.getPreReleaseMessage(this.extension);
if (preReleaseMessage) {
markdown.appendMarkdown(preReleaseMessage);
markdown.appendText(`\n`);
}
const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension);
const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension);
const extensionStatus = this.extensionStatusAction.status;
const reloadRequiredMessage = this.reloadAction.enabled ? this.reloadAction.tooltip : '';
const recommendationMessage = this.getRecommendationMessage(this.extension);
if (extensionRuntimeStatus || extensionStatus || reloadRequiredMessage || recommendationMessage) {
if (extensionRuntimeStatus || extensionStatus || reloadRequiredMessage || recommendationMessage || preReleaseMessage) {
markdown.appendMarkdown(`---`);
markdown.appendText(`\n`);
@ -554,6 +549,12 @@ export class ExtensionHoverWidget extends ExtensionWidget {
markdown.appendText(`\n`);
}
if (preReleaseMessage) {
const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor);
markdown.appendMarkdown(`<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span>&nbsp;${preReleaseMessage}`);
markdown.appendText(`\n`);
}
if (recommendationMessage) {
markdown.appendMarkdown(recommendationMessage);
markdown.appendText(`\n`);
@ -575,17 +576,15 @@ export class ExtensionHoverWidget extends ExtensionWidget {
return `<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${starEmptyIcon.id})</span>&nbsp;${recommendation.reasonText}`;
}
private getPreReleaseMessage(extension: IExtension): string | undefined {
static getPreReleaseMessage(extension: IExtension): string | undefined {
if (!extension.hasPreReleaseVersion) {
return undefined;
}
if (extension.state === ExtensionState.Installed && extension.local?.isPreReleaseVersion) {
if (extension.local?.isPreReleaseVersion || extension.gallery?.properties.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', "This extension has a {0} available", preReleaseVersionLink);
return `<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span>&nbsp;${message}`;
const preReleaseVersionLink = `[${localize('Show prerelease version', "Pre-Release version")}](${URI.parse(`command:workbench.extensions.action.showPreReleaseVersion?${encodeURIComponent(JSON.stringify([extension.identifier.id]))}`)})`;
return localize('has prerelease', "This extension has a {0} available", preReleaseVersionLink);
}
}

View file

@ -14,10 +14,10 @@ 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, isIExtensionIdentifier
InstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, isIExtensionIdentifier, IExtensionsControlManifest
} 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';
import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IHostService } from 'vs/workbench/services/host/browser/host';
@ -186,6 +186,7 @@ class Extension implements IExtension {
}
public isMalicious: boolean = false;
public isUnsupported: boolean | { preReleaseExtension: { id: string, displayName: string } } = false;
get installCount(): number | undefined {
return this.gallery ? this.gallery.installCount : undefined;
@ -418,28 +419,43 @@ class Extensions extends Disposable {
return this.local;
}
async syncLocalWithGalleryExtension(gallery: IGalleryExtension, maliciousExtensionSet: Set<string>): Promise<boolean> {
async syncLocalWithGalleryExtension(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): Promise<boolean> {
const extension = this.getInstalledExtensionMatchingGallery(gallery);
if (!extension) {
if (!extension?.local) {
return false;
}
if (maliciousExtensionSet.has(extension.identifier.id)) {
extension.isMalicious = true;
let hasChanged: boolean = false;
const isMalicious = extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier));
if (extension.isMalicious !== isMalicious) {
extension.isMalicious = isMalicious;
hasChanged = true;
}
const compatible = await this.getCompatibleExtension(gallery, !!extension.local?.isPreReleaseVersion);
if (!compatible) {
return false;
}
// Sync the local extension with gallery extension if local extension doesnot has metadata
if (extension.local) {
if (compatible) {
const local = extension.local.identifier.uuid ? extension.local : await this.server.extensionManagementService.updateMetadata(extension.local, { id: compatible.identifier.uuid, publisherDisplayName: compatible.publisherDisplayName, publisherId: compatible.publisherId });
extension.local = local;
extension.gallery = compatible;
this._onChange.fire({ extension });
return true;
hasChanged = true;
}
return false;
const unsupportedPreRelease = extensionsControlManifest.unsupportedPreReleaseExtensions ? extensionsControlManifest.unsupportedPreReleaseExtensions[extension.identifier.id.toLowerCase()] : undefined;
if (unsupportedPreRelease) {
if (isBoolean(extension.isUnsupported) || !areSameExtensions({ id: extension.isUnsupported.preReleaseExtension.id }, { id: unsupportedPreRelease.id })) {
extension.isUnsupported = { preReleaseExtension: unsupportedPreRelease };
hasChanged = true;
}
} else if (extension.isUnsupported) {
extension.isUnsupported = false;
hasChanged = true;
}
if (hasChanged) {
this._onChange.fire({ extension });
}
return hasChanged;
}
private async getCompatibleExtension(extensionOrIdentifier: IGalleryExtension | IExtensionIdentifier, includePreRelease: boolean): Promise<IGalleryExtension | null> {
@ -749,11 +765,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
options.text = options.text ? this.resolveQueryText(options.text) : options.text;
options.includePreRelease = isUndefined(options.includePreRelease) ? this.preferPreReleases : options.includePreRelease;
const report = await this.extensionManagementService.getExtensionsReport();
const maliciousSet = getMaliciousExtensionsSet(report);
const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest();
try {
const result = await this.galleryService.query(options, token);
return mapPager(result, gallery => this.fromGallery(gallery, maliciousSet));
return mapPager(result, gallery => this.fromGallery(gallery, extensionsControlManifest));
} catch (error) {
if (/No extension gallery service configured/.test(error.message)) {
return Promise.resolve(singlePagePager([]));
@ -890,11 +905,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return extension || extensions[0];
}
private fromGallery(gallery: IGalleryExtension, maliciousExtensionSet: Set<string>): IExtension {
private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension {
Promise.all([
this.localExtensions ? this.localExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false),
this.remoteExtensions ? this.remoteExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false),
this.webExtensions ? this.webExtensions.syncLocalWithGalleryExtension(gallery, maliciousExtensionSet) : Promise.resolve(false)
this.localExtensions ? this.localExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false),
this.remoteExtensions ? this.remoteExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false),
this.webExtensions ? this.webExtensions.syncLocalWithGalleryExtension(gallery, extensionsControlManifest) : Promise.resolve(false)
])
.then(result => {
if (result[0] || result[1] || result[2]) {
@ -907,9 +922,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return installed;
}
const extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), undefined, undefined, gallery);
if (maliciousExtensionSet.has(extension.identifier.id)) {
if (extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier))) {
extension.isMalicious = true;
}
const unsupportedPreRelease = extensionsControlManifest.unsupportedPreReleaseExtensions ? extensionsControlManifest.unsupportedPreReleaseExtensions[extension.identifier.id.toLowerCase()] : undefined;
if (unsupportedPreRelease) {
if (isBoolean(extension.isUnsupported) || !areSameExtensions({ id: extension.isUnsupported.preReleaseExtension.id }, { id: unsupportedPreRelease.id })) {
extension.isUnsupported = { preReleaseExtension: unsupportedPreRelease };
}
}
return extension;
}
@ -1031,6 +1052,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return false;
}
if (extension.isUnsupported) {
return false;
}
if (!extension.gallery) {
return false;
}

View file

@ -261,13 +261,32 @@
margin-top: 2px;
}
.extension-editor > .header > .details > .pre-release-text p,
.extension-editor > .header > .details > .actions-status-container > .status > .status-text p {
margin-top: 0px;
margin-bottom: 0px;
}
.extension-editor > .header > .details > .pre-release-text a,
.extension-editor > .header > .details > .actions-status-container > .status > .status-text a {
color: var(--vscode-textLink-foreground)
}
.extension-editor > .header > .details > .pre-release-text a:hover,
.extension-editor > .header > .details > .actions-status-container > .status > .status-text a:hover {
text-decoration: underline;
color: var(--vscode-textLink-activeForeground)
}
.extension-editor > .header > .details > .pre-release-text a:active,
.extension-editor > .header > .details > .actions-status-container > .status > .status-text a:active {
color: var(--vscode-textLink-activeForeground)
}
.extension-editor > .header > .details > .pre-release-text:not(:empty){
margin-top: 5px;
display: flex;
font-size: 90%;
}
.extension-editor > .header > .details > .recommendation {

View file

@ -60,6 +60,6 @@
}
/* codicon colors */
.codicon .codicon-extensions-pre-release {
.codicon.codicon-extensions-pre-release {
color: var(--vscode-extensionIcon-preReleaseForeground);
}

View file

@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { SwitchUnsupportedExtensionToPreReleaseExtensionCommandAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
export class UnsupportedPreReleaseExtensionsChecker implements IWorkbenchContribution {
constructor(
@INotificationService private readonly notificationService: INotificationService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
this.notifyUnsupportedPreReleaseExtensions();
}
private async notifyUnsupportedPreReleaseExtensions(): Promise<void> {
const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest();
if (!extensionsControlManifest.unsupportedPreReleaseExtensions) {
return;
}
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
const unsupportedLocalExtensionsWithIdentifiers: [ILocalExtension, IExtensionIdentifier][] = [];
for (const extension of installed) {
const preReleaseExtension = extensionsControlManifest.unsupportedPreReleaseExtensions[extension.identifier.id.toLowerCase()];
if (preReleaseExtension) {
unsupportedLocalExtensionsWithIdentifiers.push([extension, { id: preReleaseExtension.id }]);
}
}
if (!unsupportedLocalExtensionsWithIdentifiers.length) {
return;
}
const unsupportedPreReleaseExtensions: [ILocalExtension, IGalleryExtension][] = [];
const galleryExensions = await this.extensionGalleryService.getExtensions(unsupportedLocalExtensionsWithIdentifiers.map(([, identifier]) => identifier), true, CancellationToken.None);
for (const gallery of galleryExensions) {
const unsupportedLocalExtension = unsupportedLocalExtensionsWithIdentifiers.find(([, identifier]) => areSameExtensions(identifier, gallery.identifier));
if (unsupportedLocalExtension) {
unsupportedPreReleaseExtensions.push([unsupportedLocalExtension[0], gallery]);
}
}
if (!unsupportedPreReleaseExtensions.length) {
return;
}
if (unsupportedPreReleaseExtensions.length === 1) {
const [local, gallery] = unsupportedPreReleaseExtensions[0];
const action = this.instantiationService.createInstance(SwitchUnsupportedExtensionToPreReleaseExtensionCommandAction, unsupportedPreReleaseExtensions[0][0], unsupportedPreReleaseExtensions[0][1], true);
this.notificationService.notify({
severity: Severity.Info,
message: localize('unsupported prerelease message', "'{0}' extension is no longer supported and is now part of the '{1}' extension as a pre-release version. Would you like to switch to it?", local.manifest.displayName || local.identifier.id, gallery.displayName, gallery.displayName),
actions: {
primary: [action]
},
sticky: true
});
return;
}
}
}

View file

@ -76,6 +76,7 @@ export interface IExtension {
readonly local?: ILocalExtension;
gallery?: IGalleryExtension;
readonly isMalicious: boolean;
readonly isUnsupported: boolean | { preReleaseExtension: { id: string, displayName: string } };
}
export const SERVICE_ID = 'extensionsWorkbenchService';

View file

@ -218,7 +218,7 @@ suite('ExtensionRecommendationsService Test', () => {
onDidUninstallExtension: didUninstallEvent.event,
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
async getExtensionsControlManifest() { return { malicious: [] }; },
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
});
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{

View file

@ -101,7 +101,7 @@ async function setupTest() {
onUninstallExtension: uninstallEvent.event,
onDidUninstallExtension: didUninstallEvent.event,
async getInstalled() { return []; },
async getExtensionsReport() { return []; },
async getExtensionsControlManifest() { return { malicious: [] }; },
async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata) {
local.identifier.uuid = metadata.id;
local.publisherDisplayName = metadata.publisherDisplayName;

View file

@ -98,7 +98,7 @@ suite('ExtensionsListView Tests', () => {
onDidUninstallExtension: didUninstallEvent.event,
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
async getExtensionsControlManifest() { return { malicious: [] }; },
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
});
instantiationService.stub(IRemoteAgentService, RemoteAgentService);
@ -163,7 +163,7 @@ suite('ExtensionsListView Tests', () => {
setup(async () => {
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [localEnabledTheme, localEnabledLanguage, localRandom, localDisabledTheme, localDisabledLanguage, builtInTheme, builtInBasic]);
instantiationService.stubPromise(IExtensionManagementService, 'getExtensionsReport', []);
instantiationService.stubPromise(IExtensionManagementService, 'getExtensgetExtensionsControlManifestionsReport', {});
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage());
instantiationService.stubPromise(IExperimentService, 'getExperimentsByType', []);

View file

@ -94,7 +94,7 @@ suite('ExtensionsWorkbenchServiceTest', () => {
onUninstallExtension: uninstallEvent.event,
onDidUninstallExtension: didUninstallEvent.event,
async getInstalled() { return []; },
async getExtensionsReport() { return []; },
async getExtensionsControlManifest() { return { malicious: [] }; },
async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata) {
local.identifier.uuid = metadata.id;
local.publisherDisplayName = metadata.publisherDisplayName;

View file

@ -160,18 +160,20 @@ class EditorStatusContribution implements IWorkbenchContribution {
const showSeverity = first.severity >= Severity.Warning;
const text = EditorStatusContribution._severityToComboCodicon(first.severity);
let isOneBusy = false;
const ariaLabels: string[] = [];
const element = document.createElement('div');
for (const status of model.combined) {
element.appendChild(this._renderStatus(status, showSeverity, this._renderDisposables));
ariaLabels.push(this._asAriaLabel(status));
isOneBusy = isOneBusy || status.busy;
}
const props: IStatusbarEntry = {
name: localize('langStatus.name', "Editor Language Status"),
ariaLabel: localize('langStatus.aria', "Editor Language Status: {0}", ariaLabels.join(', next: ')),
tooltip: element,
command: ShowTooltipCommand,
text,
text: isOneBusy ? `${text}\u00A0\u00A0$(sync~spin)` : text,
};
if (!this._combinedEntry) {
this._combinedEntry = this._statusBarService.addEntry(props, EditorStatusContribution._id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.LEFT, compact: true });
@ -219,7 +221,7 @@ class EditorStatusContribution implements IWorkbenchContribution {
const label = document.createElement('span');
label.classList.add('label');
dom.append(label, ...renderLabelWithIcons(status.label));
dom.append(label, ...renderLabelWithIcons(status.busy ? `$(sync~spin)\u00A0\u00A0${status.label}` : status.label));
left.appendChild(label);
const detail = document.createElement('span');
@ -311,7 +313,7 @@ class EditorStatusContribution implements IWorkbenchContribution {
return {
name: localize('name.pattern', '{0} (Language Status)', item.name),
text: item.label,
text: item.busy ? `${item.label}\u00A0\u00A0$(sync~spin)` : item.label,
ariaLabel: item.accessibilityInfo?.label ?? item.label,
role: item.accessibilityInfo?.role,
tooltip: item.command?.tooltip || new MarkdownString(item.detail, true),

View file

@ -200,7 +200,7 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon
remoteAgentService.getEnvironment().then(environment => {
if (environment?.os !== OperatingSystem.Linux) {
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
.registerDefaultConfigurations([{ 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT }]);
.registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]);
this._register(new OutputAutomaticPortForwarding(terminalService, notificationService, openerService, externalOpenerService,
remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, logService, () => false));
} else {

View file

@ -25,6 +25,10 @@ import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/d
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { firstOrDefault } from 'vs/base/common/arrays';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { PersistentConnection } from 'vs/platform/remote/common/remoteAgentConnection';
export class LabelContribution implements IWorkbenchContribution {
constructor(
@ -161,6 +165,43 @@ workbenchContributionsRegistry.registerWorkbenchContribution(RemoteLogOutputChan
workbenchContributionsRegistry.registerWorkbenchContribution(TunnelFactoryContribution, LifecyclePhase.Ready);
workbenchContributionsRegistry.registerWorkbenchContribution(ShowCandidateContribution, LifecyclePhase.Ready);
const enableDiagnostics = true;
if (enableDiagnostics) {
class TriggerReconnectAction extends Action2 {
constructor() {
super({
id: 'workbench.action.triggerReconnect',
title: { value: localize('triggerReconnect', "Connection: Trigger Reconnect"), original: 'Connection: Trigger Reconnect' },
category: CATEGORIES.Developer,
f1: true,
});
}
async run(accessor: ServicesAccessor): Promise<void> {
PersistentConnection.debugTriggerReconnection();
}
}
class PauseSocketWriting extends Action2 {
constructor() {
super({
id: 'workbench.action.pauseSocketWriting',
title: { value: localize('pauseSocketWriting', "Connection: Pause socket writing"), original: 'Connection: Pause socket writing' },
category: CATEGORIES.Developer,
f1: true,
});
}
async run(accessor: ServicesAccessor): Promise<void> {
PersistentConnection.debugPauseSocketWriting();
}
}
registerAction2(TriggerReconnectAction);
registerAction2(PauseSocketWriting);
}
const extensionKindSchema: IJSONSchema = {
type: 'string',
enum: [

View file

@ -1162,7 +1162,7 @@ export class DirtyDiffModel extends Disposable {
}
private diff(): Promise<IChange[] | null> {
return this.progressService.withProgress({ location: ProgressLocation.Scm }, async () => {
return this.progressService.withProgress({ location: ProgressLocation.Scm, delay: 250 }, async () => {
return this.getOriginalURIPromise().then(originalURI => {
if (this._disposed || this._model.isDisposed() || !originalURI) {
return Promise.resolve([]); // disposed

View file

@ -18,6 +18,7 @@ import { TestResultService } from 'vs/workbench/contrib/testing/common/testResul
import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { Convert, getInitializedMainTestCollection, TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/common/testStubs';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { isNative, isElectron } from 'vs/base/common/platform';
export const emptyOutputController = () => new LiveOutputController(
new Lazy(() => [newWriteableBufferStream(), Promise.resolve()]),
@ -311,7 +312,7 @@ suite('Workbench - Test Results Service', () => {
});
});
test('resultItemParents', () => {
((isNative && !isElectron) ? test.skip /* TODO@connor4312 https://github.com/microsoft/vscode/issues/137853 */ : test)('resultItemParents', function () {
assert.deepStrictEqual([...resultItemParents(r, r.getStateById(new TestId(['ctrlId', 'id-a', 'id-aa']).toString())!)], [
r.getStateById(new TestId(['ctrlId', 'id-a', 'id-aa']).toString()),
r.getStateById(new TestId(['ctrlId', 'id-a']).toString()),

View file

@ -4,12 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/common/actions';
import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IColorRegistry, Extensions as ColorRegistryExtensions } from 'vs/platform/theme/common/colorRegistry';
@ -31,211 +30,15 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { Emitter } from 'vs/base/common/event';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
export const manageExtensionIcon = registerIcon('theme-selection-manage-extension', Codicon.gear, localize('manageExtensionIcon', 'Icon for the \'Manage\' action in the theme selection quick pick.'));
export class SelectColorThemeAction extends Action {
type PickerResult = 'back' | 'selected' | 'cancelled';
static readonly ID = 'workbench.action.selectTheme';
static readonly LABEL = localize('selectTheme.label', "Color Theme");
static readonly INSTALL_ADDITIONAL = localize('installColorThemes', "Install Additional Color Themes...");
static readonly BROWSE_ADDITIONAL = '$(plus) ' + localize('browseColorThemes', "Browse Additional Color Themes...");
constructor(
id: string,
label: string,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService,
@ILogService private readonly logService: ILogService,
@IProgressService private progressService: IProgressService
) {
super(id, label);
}
override run(): Promise<void> {
return this.themeService.getColorThemes().then(themes => {
const currentTheme = this.themeService.getColorTheme();
const supportsGallery = this.extensionGalleryService.isEnabled();
const supportsGalleryPreview = supportsGallery && this.extensionResourceLoaderService.supportsExtensionGalleryResources;
const picks: QuickPickInput<ThemeItem>[] = [
...(supportsGalleryPreview ? configurationEntries(SelectColorThemeAction.BROWSE_ADDITIONAL) : []),
...toEntries(themes.filter(t => t.type === ColorScheme.LIGHT), localize('themes.category.light', "light themes")),
...toEntries(themes.filter(t => t.type === ColorScheme.DARK), localize('themes.category.dark', "dark themes")),
...toEntries(themes.filter(t => t.type === ColorScheme.HIGH_CONTRAST), localize('themes.category.hc', "high contrast themes")),
...(supportsGallery && !supportsGalleryPreview ? configurationEntries(SelectColorThemeAction.INSTALL_ADDITIONAL) : []),
];
let selectThemeTimeout: number | undefined;
const selectTheme = (theme: IWorkbenchTheme | undefined, applyTheme: boolean) => {
if (selectThemeTimeout) {
clearTimeout(selectThemeTimeout);
}
selectThemeTimeout = window.setTimeout(() => {
selectThemeTimeout = undefined;
const newTheme = (theme ?? currentTheme) as IWorkbenchColorTheme;
this.themeService.setColorTheme(newTheme, applyTheme ? 'auto' : 'preview').then(undefined,
err => {
onUnexpectedError(err);
this.themeService.setColorTheme(currentTheme.id, undefined);
}
);
}, applyTheme ? 0 : 200);
};
return new Promise((s, _) => {
const browseInstalledThemes = (activeItemId: string | undefined) => {
let isCompleted = false;
const autoFocusIndex = picks.findIndex(p => isItem(p) && p.id === activeItemId);
const quickpick = this.quickInputService.createQuickPick<ThemeItem>();
quickpick.items = picks;
quickpick.sortByLabel = false;
quickpick.matchOnDescription = true;
quickpick.placeholder = localize('themes.selectTheme', "Select Color Theme (Up/Down Keys to Preview)");
quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem];
quickpick.canSelectMany = false;
quickpick.onDidAccept(async _ => {
const themeItem = quickpick.activeItems[0];
if (!themeItem || themeItem.theme === undefined) { // 'pick in marketplace' entry
if (supportsGalleryPreview) {
browseMarketplaceThemes(quickpick.value);
} else {
openExtensionViewlet(this.paneCompositeService, `category:themes ${quickpick.value}`);
}
} else {
let themeToSet = themeItem.theme;
selectTheme(themeToSet, true);
}
isCompleted = true;
quickpick.hide();
s();
});
quickpick.onDidTriggerItemButton(e => {
if (isItem(e.item)) {
const extensionId = e.item.theme?.extensionData?.extensionId;
if (extensionId) {
openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`);
} else {
openExtensionViewlet(this.paneCompositeService, `category:themes ${quickpick.value}`);
}
}
});
quickpick.onDidChangeActive(themes => selectTheme(themes[0]?.theme, false));
quickpick.onDidHide(() => {
if (!isCompleted) {
selectTheme(currentTheme, true);
s();
isCompleted = true;
}
});
quickpick.show();
};
const browseMarketplaceThemes = (value: string) => {
let isCompleted = false;
const marketplaceThemes = new MarketplaceThemes(this.extensionGalleryService, this.extensionManagementService, this.themeService, this.logService);
const mp_quickpick = this.quickInputService.createQuickPick<ThemeItem>();
mp_quickpick.items = [];
mp_quickpick.sortByLabel = false;
mp_quickpick.matchOnDescription = true;
mp_quickpick.buttons = [this.quickInputService.backButton];
mp_quickpick.title = 'Marketplace Themes';
mp_quickpick.placeholder = localize('themes.selectMarketplaceTheme', "Type to Search More. Select to Install. Up/Down Keys to Preview");
mp_quickpick.canSelectMany = false;
mp_quickpick.onDidChangeValue(() => marketplaceThemes.trigger(mp_quickpick.value));
mp_quickpick.onDidAccept(async _ => {
let themeItem = mp_quickpick.activeItems[0];
if (themeItem?.galleryExtension) {
isCompleted = true;
mp_quickpick.hide();
const success = await this.installExtension(themeItem.galleryExtension);
if (success) {
selectTheme(themeItem.theme, true);
}
s();
}
});
mp_quickpick.onDidTriggerItemButton(e => {
if (isItem(e.item)) {
const extensionId = e.item.theme?.extensionData?.extensionId;
if (extensionId) {
openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`);
} else {
openExtensionViewlet(this.paneCompositeService, `category:themes ${mp_quickpick.value}`);
}
}
});
mp_quickpick.onDidChangeActive(themes => selectTheme(themes[0]?.theme, false));
mp_quickpick.onDidHide(() => {
if (!isCompleted) {
selectTheme(currentTheme, true);
isCompleted = true;
}
marketplaceThemes.dispose();
});
mp_quickpick.onDidTriggerButton(e => {
if (e === this.quickInputService.backButton) {
mp_quickpick.hide();
browseInstalledThemes(undefined);
}
});
marketplaceThemes.onDidChange(() => {
let items = marketplaceThemes.themes;
if (marketplaceThemes.isSearching) {
items = items.concat({ label: '$(sync~spin) Searching for themes...', id: undefined, alwaysShow: true });
}
const activeItemId = mp_quickpick.activeItems[0]?.id;
const newActiveItem = activeItemId ? items.find(i => isItem(i) && i.id === activeItemId) : undefined;
mp_quickpick.items = items;
if (newActiveItem) {
mp_quickpick.activeItems = [newActiveItem as ThemeItem];
}
});
marketplaceThemes.trigger(value);
mp_quickpick.show();
};
browseInstalledThemes(currentTheme.id);
});
});
}
private async installExtension(galleryExtension: IGalleryExtension) {
try {
openExtensionViewlet(this.paneCompositeService, `@id:${galleryExtension.identifier.id}`);
await this.progressService.withProgress({
location: ProgressLocation.Notification,
title: localize('installing extensions', "Installing Extension {0}...", galleryExtension.displayName)
}, async () => {
await this.extensionManagementService.installFromGallery(galleryExtension);
});
return true;
} catch (e) {
this.logService.error(`Problem installing extension ${galleryExtension.identifier.id}`, e);
return false;
}
}
}
class MarketplaceThemes {
class MarketplaceThemesPicker {
private readonly _installedExtensions: Promise<Set<string>>;
private readonly _marketplaceExtensions: Set<string> = new Set();
private readonly _marketplaceThemes: ThemeItem[] = [];
@ -247,10 +50,15 @@ class MarketplaceThemes {
private readonly _queryDelayer = new ThrottledDelayer<void>(200);
constructor(
private extensionGalleryService: IExtensionGalleryService,
extensionManagementService: IExtensionManagementService,
private themeService: IWorkbenchThemeService,
private logService: ILogService
private readonly getMarketplaceColorThemes: (publisher: string, name: string, version: string) => Promise<IWorkbenchTheme[]>,
private readonly marketplaceQuery: string,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@ILogService private readonly logService: ILogService,
@IProgressService private readonly progressService: IProgressService,
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService
) {
this._installedExtensions = extensionManagementService.getInstalled().then(installed => {
const result = new Set<string>();
@ -290,7 +98,7 @@ class MarketplaceThemes {
try {
const installedExtensions = await this._installedExtensions;
const options = { text: `category:themes ${value}`, pageSize: 10 };
const options = { text: `${this.marketplaceQuery} ${value}`, pageSize: 20 };
const pager = await this.extensionGalleryService.query(options, token);
for (let i = 0; i < pager.total && i < 2; i++) {
if (token.isCancellationRequested) {
@ -307,7 +115,7 @@ class MarketplaceThemes {
const ext = gallery[i];
if (!installedExtensions.has(ext.identifier.id) && !this._marketplaceExtensions.has(ext.identifier.id)) {
this._marketplaceExtensions.add(ext.identifier.id);
const themes = await this.themeService.getMarketplaceColorThemes(ext.identifier.id, ext.version);
const themes = await this.getMarketplaceColorThemes(ext.publisher, ext.name, ext.version);
for (const theme of themes) {
this._marketplaceThemes.push({ id: theme.id, theme: theme, label: theme.label, description: `${ext.displayName} · ${ext.publisherDisplayName}`, galleryExtension: ext, buttons: [configureButton] });
}
@ -322,11 +130,101 @@ class MarketplaceThemes {
if (!isPromiseCanceledError(e)) {
this.logService.error(`Error while searching for themes:`, e);
}
} finally {
this._searchOngoing = false;
this._onDidChange.fire();
}
this._searchOngoing = false;
this._onDidChange.fire();
}
public openQuickPick(value: string, currentTheme: IWorkbenchTheme | undefined, selectTheme: (theme: IWorkbenchTheme | undefined, applyTheme: boolean) => void): Promise<PickerResult> {
let result: PickerResult | undefined = undefined;
return new Promise<PickerResult>((s, _) => {
const quickpick = this.quickInputService.createQuickPick<ThemeItem>();
quickpick.items = [];
quickpick.sortByLabel = false;
quickpick.matchOnDescription = true;
quickpick.buttons = [this.quickInputService.backButton];
quickpick.title = 'Marketplace Themes';
quickpick.placeholder = localize('themes.selectMarketplaceTheme', "Type to Search More. Select to Install. Up/Down Keys to Preview");
quickpick.canSelectMany = false;
quickpick.onDidChangeValue(() => this.trigger(quickpick.value));
quickpick.onDidAccept(async _ => {
let themeItem = quickpick.selectedItems[0];
if (themeItem?.galleryExtension) {
result = 'selected';
quickpick.hide();
const success = await this.installExtension(themeItem.galleryExtension);
if (success) {
selectTheme(themeItem.theme, true);
}
}
});
quickpick.onDidTriggerItemButton(e => {
if (isItem(e.item)) {
const extensionId = e.item.theme?.extensionData?.extensionId;
if (extensionId) {
openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`);
} else {
openExtensionViewlet(this.paneCompositeService, `${this.marketplaceQuery} ${quickpick.value}`);
}
}
});
quickpick.onDidChangeActive(themes => selectTheme(themes[0]?.theme, false));
quickpick.onDidHide(() => {
if (result === undefined) {
selectTheme(currentTheme, true);
result = 'cancelled';
}
quickpick.dispose();
s(result);
});
quickpick.onDidTriggerButton(e => {
if (e === this.quickInputService.backButton) {
result = 'back';
quickpick.hide();
}
});
this.onDidChange(() => {
let items = this.themes;
if (this.isSearching) {
items = items.concat({ label: '$(sync~spin) Searching for themes...', id: undefined, alwaysShow: true });
}
const activeItemId = quickpick.activeItems[0]?.id;
const newActiveItem = activeItemId ? items.find(i => isItem(i) && i.id === activeItemId) : undefined;
quickpick.items = items;
if (newActiveItem) {
quickpick.activeItems = [newActiveItem as ThemeItem];
}
});
this.trigger(value);
quickpick.show();
});
}
private async installExtension(galleryExtension: IGalleryExtension) {
try {
openExtensionViewlet(this.paneCompositeService, `@id:${galleryExtension.identifier.id}`);
await this.progressService.withProgress({
location: ProgressLocation.Notification,
title: localize('installing extensions', "Installing Extension {0}...", galleryExtension.displayName)
}, async () => {
await this.extensionManagementService.installFromGallery(galleryExtension);
});
return true;
} catch (e) {
this.logService.error(`Problem installing extension ${galleryExtension.identifier.id}`, e);
return false;
}
}
public dispose() {
if (this._tokenSource) {
this._tokenSource.cancel();
@ -338,144 +236,240 @@ class MarketplaceThemes {
}
}
abstract class AbstractIconThemeAction extends Action {
constructor(
id: string,
label: string,
private readonly quickInputService: IQuickInputService,
private readonly extensionGalleryService: IExtensionGalleryService,
private readonly paneCompositeService: IPaneCompositePartService
class InstalledThemesPicker {
constructor(
private readonly installMessage: string,
private readonly browseMessage: string,
private readonly placeholderMessage: string,
private readonly marketplaceTag: string,
private readonly setTheme: (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => Promise<any>,
private readonly getMarketplaceColorThemes: (publisher: string, name: string, version: string) => Promise<IWorkbenchTheme[]>,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
@IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super(id, label);
}
protected abstract get builtInEntry(): QuickPickInput<ThemeItem>;
protected abstract get installMessage(): string;
protected abstract get placeholderMessage(): string;
protected abstract get marketplaceTag(): string;
protected abstract setTheme(id: string, settingsTarget: ThemeSettingTarget): Promise<any>;
protected pick(themes: IWorkbenchTheme[], currentTheme: IWorkbenchTheme) {
let picks: QuickPickInput<ThemeItem>[] = [this.builtInEntry, ...toEntries(themes), ...configurationEntries(this.installMessage)];
public async openQuickPick(picks: QuickPickInput<ThemeItem>[], currentTheme: IWorkbenchTheme) {
let marketplaceThemePicker: MarketplaceThemesPicker | undefined;
if (this.extensionGalleryService.isEnabled()) {
if (this.extensionResourceLoaderService.supportsExtensionGalleryResources) {
marketplaceThemePicker = this.instantiationService.createInstance(MarketplaceThemesPicker, this.getMarketplaceColorThemes.bind(this), this.marketplaceTag);
picks = [...configurationEntries(this.browseMessage), ...picks];
} else {
picks = [...picks, ...configurationEntries(this.installMessage)];
}
}
let selectThemeTimeout: number | undefined;
const selectTheme = (theme: ThemeItem, applyTheme: boolean) => {
const selectTheme = (theme: IWorkbenchTheme | undefined, applyTheme: boolean) => {
if (selectThemeTimeout) {
clearTimeout(selectThemeTimeout);
}
selectThemeTimeout = window.setTimeout(() => {
selectThemeTimeout = undefined;
const themeId = theme && theme.id !== undefined ? theme.id : currentTheme.id;
this.setTheme(themeId, applyTheme ? 'auto' : 'preview').then(undefined,
const newTheme = (theme ?? currentTheme) as IWorkbenchTheme;
this.setTheme(newTheme, applyTheme ? 'auto' : 'preview').then(undefined,
err => {
onUnexpectedError(err);
this.setTheme(currentTheme.id, undefined);
this.setTheme(currentTheme, undefined);
}
);
}, applyTheme ? 0 : 200);
};
return new Promise<void>((s, _) => {
let isCompleted = false;
const pickInstalledThemes = (activeItemId: string | undefined) => {
return new Promise<void>((s, _) => {
let isCompleted = false;
const autoFocusIndex = picks.findIndex(p => isItem(p) && p.id === currentTheme.id);
const quickpick = this.quickInputService.createQuickPick<ThemeItem>();
quickpick.items = this.extensionGalleryService.isEnabled() ? picks.concat(configurationEntries(this.installMessage)) : picks;
quickpick.placeholder = this.placeholderMessage;
quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem];
quickpick.canSelectMany = false;
quickpick.onDidAccept(_ => {
const theme = quickpick.activeItems[0];
if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry
openExtensionViewlet(this.paneCompositeService, `${this.marketplaceTag} ${quickpick.value}`);
} else {
selectTheme(theme, true);
}
isCompleted = true;
quickpick.hide();
s();
});
quickpick.onDidChangeActive(themes => selectTheme(themes[0], false));
quickpick.onDidHide(() => {
if (!isCompleted) {
selectTheme(currentTheme, true);
const autoFocusIndex = picks.findIndex(p => isItem(p) && p.id === activeItemId);
const quickpick = this.quickInputService.createQuickPick<ThemeItem>();
quickpick.items = picks;
quickpick.placeholder = this.placeholderMessage;
quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem];
quickpick.canSelectMany = false;
quickpick.onDidAccept(async _ => {
isCompleted = true;
const theme = quickpick.selectedItems[0];
if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry
if (marketplaceThemePicker) {
const res = await marketplaceThemePicker.openQuickPick(quickpick.value, currentTheme, selectTheme);
if (res === 'back') {
await pickInstalledThemes(undefined);
}
} else {
openExtensionViewlet(this.paneCompositeService, `${this.marketplaceTag} ${quickpick.value}`);
}
} else {
selectTheme(theme.theme, true);
}
quickpick.hide();
s();
}
});
quickpick.onDidChangeActive(themes => selectTheme(themes[0].theme, false));
quickpick.onDidHide(() => {
if (!isCompleted) {
selectTheme(currentTheme, true);
s();
}
quickpick.dispose();
});
quickpick.onDidTriggerItemButton(e => {
if (isItem(e.item)) {
const extensionId = e.item.theme?.extensionData?.extensionId;
if (extensionId) {
openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`);
} else {
openExtensionViewlet(this.paneCompositeService, `${this.marketplaceTag} ${quickpick.value}`);
}
}
});
quickpick.show();
});
quickpick.show();
};
await pickInstalledThemes(currentTheme.id);
marketplaceThemePicker?.dispose();
}
}
const SelectColorThemeCommandId = 'workbench.action.selectTheme';
registerAction2(class extends Action2 {
constructor() {
super({
id: SelectColorThemeCommandId,
title: localize('selectTheme.label', "Color Theme"),
category: CATEGORIES.Preferences,
f1: true,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyT)
}
});
}
}
class SelectFileIconThemeAction extends AbstractIconThemeAction {
override async run(accessor: ServicesAccessor) {
const themeService = accessor.get(IWorkbenchThemeService);
static readonly ID = 'workbench.action.selectIconTheme';
static readonly LABEL = localize('selectIconTheme.label', "File Icon Theme");
const installMessage = localize('installColorThemes', "Install Additional Color Themes...");
const browseMessage = '$(plus) ' + localize('browseColorThemes', "Browse Additional Color Themes...");
const placeholderMessage = localize('themes.selectTheme', "Select Color Theme (Up/Down Keys to Preview)");
const marketplaceTag = 'category:themes';
const setTheme = (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => themeService.setColorTheme(theme as IWorkbenchColorTheme, settingsTarget);
const getMarketplaceColorThemes = (publisher: string, name: string, version: string) => themeService.getMarketplaceColorThemes(publisher, name, version);
constructor(
id: string,
label: string,
@IQuickInputService quickInputService: IQuickInputService,
@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
@IPaneCompositePartService paneCompositeService: IPaneCompositePartService
const instantiationService = accessor.get(IInstantiationService);
const picker = instantiationService.createInstance(InstalledThemesPicker, installMessage, browseMessage, placeholderMessage, marketplaceTag, setTheme, getMarketplaceColorThemes);
) {
super(id, label, quickInputService, extensionGalleryService, paneCompositeService);
const themes = await themeService.getColorThemes();
const currentTheme = themeService.getColorTheme();
const picks: QuickPickInput<ThemeItem>[] = [
...toEntries(themes.filter(t => t.type === ColorScheme.LIGHT), localize('themes.category.light', "light themes")),
...toEntries(themes.filter(t => t.type === ColorScheme.DARK), localize('themes.category.dark', "dark themes")),
...toEntries(themes.filter(t => t.type === ColorScheme.HIGH_CONTRAST), localize('themes.category.hc', "high contrast themes")),
];
await picker.openQuickPick(picks, currentTheme);
}
});
const SelectFileIconThemeCommandId = 'workbench.action.selectIconTheme';
registerAction2(class extends Action2 {
constructor() {
super({
id: SelectFileIconThemeCommandId,
title: localize('selectIconTheme.label', "File Icon Theme"),
category: CATEGORIES.Preferences,
f1: true
});
}
protected builtInEntry: QuickPickInput<ThemeItem> = { id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable File Icons') };
protected installMessage = localize('installIconThemes', "Install Additional File Icon Themes...");
protected placeholderMessage = localize('themes.selectIconTheme', "Select File Icon Theme");
protected marketplaceTag = 'tag:icon-theme';
protected setTheme(id: string, settingsTarget: ThemeSettingTarget) {
return this.themeService.setFileIconTheme(id, settingsTarget);
override async run(accessor: ServicesAccessor) {
const themeService = accessor.get(IWorkbenchThemeService);
const installMessage = localize('installIconThemes', "Install Additional File Icon Themes...");
const browseMessage = '$(plus) ' + localize('browseIconThemes', "Browse Additional File Icon Themes...");
const placeholderMessage = localize('themes.selectIconTheme', "Select File Icon Theme (Up/Down Keys to Preview)");
const marketplaceTag = 'tag:icon-theme';
const setTheme = (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => themeService.setFileIconTheme(theme as IWorkbenchFileIconTheme, settingsTarget);
const getMarketplaceColorThemes = (publisher: string, name: string, version: string) => themeService.getMarketplaceFileIconThemes(publisher, name, version);
const instantiationService = accessor.get(IInstantiationService);
const picker = instantiationService.createInstance(InstalledThemesPicker, installMessage, browseMessage, placeholderMessage, marketplaceTag, setTheme, getMarketplaceColorThemes);
const picks: QuickPickInput<ThemeItem>[] = [
{ type: 'separator', label: localize('fileIconThemeCategory', 'file icon themes') },
{ id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable File Icons') },
...toEntries(await themeService.getFileIconThemes()),
];
await picker.openQuickPick(picks, themeService.getFileIconTheme());
}
});
const SelectProductIconThemeCommandId = 'workbench.action.selectProductIconTheme';
registerAction2(class extends Action2 {
constructor() {
super({
id: SelectProductIconThemeCommandId,
title: localize('selectProductIconTheme.label', "Product Icon Theme"),
category: CATEGORIES.Preferences,
f1: true
});
}
override async run(): Promise<void> {
this.pick(await this.themeService.getFileIconThemes(), this.themeService.getFileIconTheme());
override async run(accessor: ServicesAccessor) {
const themeService = accessor.get(IWorkbenchThemeService);
const installMessage = localize('installProductIconThemes', "Install Additional Product Icon Themes...");
const browseMessage = '$(plus) ' + localize('browseProductIconThemes', "Browse Additional Product Icon Themes...");
const placeholderMessage = localize('themes.selectProductIconTheme', "Select Product Icon Theme (Up/Down Keys to Preview)");
const marketplaceTag = 'tag:product-icon-theme';
const setTheme = (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => themeService.setProductIconTheme(theme as IWorkbenchProductIconTheme, settingsTarget);
const getMarketplaceColorThemes = (publisher: string, name: string, version: string) => themeService.getMarketplaceProductIconThemes(publisher, name, version);
const instantiationService = accessor.get(IInstantiationService);
const picker = instantiationService.createInstance(InstalledThemesPicker, installMessage, browseMessage, placeholderMessage, marketplaceTag, setTheme, getMarketplaceColorThemes);
const picks: QuickPickInput<ThemeItem>[] = [
{ type: 'separator', label: localize('productIconThemeCategory', 'product icon themes') },
{ id: DEFAULT_PRODUCT_ICON_THEME_ID, label: localize('defaultProductIconThemeLabel', 'Default') },
...toEntries(await themeService.getProductIconThemes()),
];
await picker.openQuickPick(picks, themeService.getProductIconTheme());
}
}
});
CommandsRegistry.registerCommand('workbench.action.previewColorTheme', async function (accessor: ServicesAccessor, extension: { publisher: string, name: string, version: string }, themeSettingsId?: string) {
const themeService = accessor.get(IWorkbenchThemeService);
class SelectProductIconThemeAction extends AbstractIconThemeAction {
static readonly ID = 'workbench.action.selectProductIconTheme';
static readonly LABEL = localize('selectProductIconTheme.label', "Product Icon Theme");
constructor(
id: string,
label: string,
@IQuickInputService quickInputService: IQuickInputService,
@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
@IPaneCompositePartService paneCompositeService: IPaneCompositePartService
) {
super(id, label, quickInputService, extensionGalleryService, paneCompositeService);
const themes = await themeService.getMarketplaceColorThemes(extension.publisher, extension.name, extension.version);
for (const theme of themes) {
if (!themeSettingsId || theme.settingsId === themeSettingsId) {
await themeService.setColorTheme(theme, 'preview');
return theme.settingsId;
}
}
protected builtInEntry: QuickPickInput<ThemeItem> = { id: DEFAULT_PRODUCT_ICON_THEME_ID, label: localize('defaultProductIconThemeLabel', 'Default') };
protected installMessage = localize('installProductIconThemes', "Install Additional Product Icon Themes...");
protected placeholderMessage = localize('themes.selectProductIconTheme', "Select Product Icon Theme");
protected marketplaceTag = 'tag:product-icon-theme';
protected setTheme(id: string, settingsTarget: ThemeSettingTarget) {
return this.themeService.setProductIconTheme(id, settingsTarget);
}
override async run(): Promise<void> {
this.pick(await this.themeService.getProductIconThemes(), this.themeService.getProductIconTheme());
}
}
return undefined;
});
function configurationEntries(label: string): QuickPickInput<ThemeItem>[] {
return [
{
type: 'separator',
label: 'marketplace themes'
type: 'separator'
},
{
id: undefined,
@ -496,12 +490,12 @@ function openExtensionViewlet(paneCompositeService: IPaneCompositePartService, q
});
}
interface ThemeItem extends IQuickPickItem {
id: string | undefined;
theme?: IWorkbenchTheme;
galleryExtension?: IGalleryExtension;
label: string;
description?: string;
alwaysShow?: boolean;
readonly id: string | undefined;
readonly theme?: IWorkbenchTheme;
readonly galleryExtension?: IGalleryExtension;
readonly label: string;
readonly description?: string;
readonly alwaysShow?: boolean;
}
function isItem(i: QuickPickInput<ThemeItem>): i is ThemeItem {
@ -529,27 +523,26 @@ const configureButton: IQuickInputButton = {
iconClass: ThemeIcon.asClassName(manageExtensionIcon),
tooltip: localize('manage extension', "Manage Extension"),
};
class GenerateColorThemeAction extends Action {
static readonly ID = 'workbench.action.generateColorTheme';
static readonly LABEL = localize('generateColorTheme.label', "Generate Color Theme From Current Settings");
constructor(
id: string,
label: string,
@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
@IEditorService private readonly editorService: IEditorService,
) {
super(id, label);
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.action.generateColorTheme',
title: localize('generateColorTheme.label', "Generate Color Theme From Current Settings"),
category: CATEGORIES.Developer,
f1: true
});
}
override run(): Promise<any> {
let theme = this.themeService.getColorTheme();
let colors = Registry.as<IColorRegistry>(ColorRegistryExtensions.ColorContribution).getColors();
let colorIds = colors.map(c => c.id).sort();
let resultingColors: { [key: string]: string | null } = {};
let inherited: string[] = [];
for (let colorId of colorIds) {
override run(accessor: ServicesAccessor) {
const themeService = accessor.get(IWorkbenchThemeService);
const theme = themeService.getColorTheme();
const colors = Registry.as<IColorRegistry>(ColorRegistryExtensions.ColorContribution).getColors();
const colorIds = colors.map(c => c.id).sort();
const resultingColors: { [key: string]: string | null } = {};
const inherited: string[] = [];
for (const colorId of colorIds) {
const color = theme.getColor(colorId, false);
if (color) {
resultingColors[colorId] = Color.Format.CSS.formatHexA(color, true);
@ -558,7 +551,7 @@ class GenerateColorThemeAction extends Action {
}
}
const nullDefaults = [];
for (let id of inherited) {
for (const id of inherited) {
const color = theme.getColor(id);
if (color) {
resultingColors['__' + id] = Color.Format.CSS.formatHexA(color, true);
@ -566,7 +559,7 @@ class GenerateColorThemeAction extends Action {
nullDefaults.push(id);
}
}
for (let id of nullDefaults) {
for (const id of nullDefaults) {
resultingColors['__' + id] = null;
}
let contents = JSON.stringify({
@ -577,29 +570,15 @@ class GenerateColorThemeAction extends Action {
}, null, '\t');
contents = contents.replace(/\"__/g, '//"');
return this.editorService.openEditor({ resource: undefined, contents, mode: 'jsonc', options: { pinned: true } });
const editorService = accessor.get(IEditorService);
return editorService.openEditor({ resource: undefined, contents, mode: 'jsonc', options: { pinned: true } });
}
}
const category = localize('preferences', "Preferences");
const colorThemeDescriptor = SyncActionDescriptor.from(SelectColorThemeAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyT) });
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(colorThemeDescriptor, 'Preferences: Color Theme', category);
const fileIconThemeDescriptor = SyncActionDescriptor.from(SelectFileIconThemeAction);
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(fileIconThemeDescriptor, 'Preferences: File Icon Theme', category);
const productIconThemeDescriptor = SyncActionDescriptor.from(SelectProductIconThemeAction);
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(productIconThemeDescriptor, 'Preferences: Product Icon Theme', category);
const generateColorThemeDescriptor = SyncActionDescriptor.from(GenerateColorThemeAction);
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(generateColorThemeDescriptor, 'Developer: Generate Color Theme From Current Settings', CATEGORIES.Developer.value);
});
MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
group: '4_themes',
command: {
id: SelectColorThemeAction.ID,
id: SelectColorThemeCommandId,
title: localize({ key: 'miSelectColorTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme")
},
order: 1
@ -608,7 +587,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
group: '4_themes',
command: {
id: SelectFileIconThemeAction.ID,
id: SelectFileIconThemeCommandId,
title: localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme")
},
order: 2
@ -617,7 +596,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
group: '4_themes',
command: {
id: SelectProductIconThemeAction.ID,
id: SelectProductIconThemeCommandId,
title: localize({ key: 'miSelectProductIconTheme', comment: ['&& denotes a mnemonic'] }, "&&Product Icon Theme")
},
order: 3
@ -627,7 +606,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '4_themes',
command: {
id: SelectColorThemeAction.ID,
id: SelectColorThemeCommandId,
title: localize('selectTheme.label', "Color Theme")
},
order: 1
@ -636,7 +615,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '4_themes',
command: {
id: SelectFileIconThemeAction.ID,
id: SelectFileIconThemeCommandId,
title: localize('themes.selectIconTheme.label', "File Icon Theme")
},
order: 2
@ -645,7 +624,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '4_themes',
command: {
id: SelectProductIconThemeAction.ID,
id: SelectProductIconThemeCommandId,
title: localize('themes.selectProductIconTheme.label', "Product Icon Theme")
},
order: 3

View file

@ -43,7 +43,7 @@ export class DefaultConfiguration extends Disposable {
) {
super();
if (environmentService.options?.configurationDefaults) {
this.configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]);
this.configurationRegistry.registerDefaultConfigurations([{ overrides: environmentService.options.configurationDefaults }]);
}
}
@ -57,6 +57,7 @@ export class DefaultConfiguration extends Disposable {
async initialize(): Promise<ConfigurationModel> {
await this.initializeCachedConfigurationDefaultsOverrides();
this._configurationModel = undefined;
this._register(this.configurationRegistry.onDidUpdateConfiguration(({ defaultsOverrides }) => this.onDidUpdateConfiguration(defaultsOverrides)));
return this.configurationModel;
}
@ -95,9 +96,9 @@ export class DefaultConfiguration extends Disposable {
private async updateCachedConfigurationDefaultsOverrides(): Promise<void> {
const cachedConfigurationDefaultsOverrides: IStringDictionary<any> = {};
const configurationDefaultsOverrides = this.configurationRegistry.getConfigurationDefaultsOverrides();
for (const key of Object.keys(configurationDefaultsOverrides)) {
if (!OVERRIDE_PROPERTY_REGEX.test(key) && configurationDefaultsOverrides[key] !== undefined) {
cachedConfigurationDefaultsOverrides[key] = configurationDefaultsOverrides[key];
for (const [key, value] of configurationDefaultsOverrides) {
if (!OVERRIDE_PROPERTY_REGEX.test(key) && value.value !== undefined) {
cachedConfigurationDefaultsOverrides[key] = value.value;
}
}
try {

View file

@ -36,6 +36,9 @@ import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/w
import { delta, distinct } from 'vs/base/common/arrays';
import { forEach, IStringDictionary } from 'vs/base/common/collections';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService';
import { isUndefined } from 'vs/base/common/types';
import { localize } from 'vs/nls';
class Workspace extends BaseWorkspace {
initialized: boolean = false;
@ -1116,6 +1119,45 @@ class ResetConfigurationDefaultsOverridesCache extends Disposable implements IWo
}
}
class UpdateExperimentalSettingsDefaults extends Disposable implements IWorkbenchContribution {
private readonly processedExperimentalSettings = new Set<string>();
private readonly configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
constructor(
@IWorkbenchAssignmentService private readonly workbenchAssignmentService: IWorkbenchAssignmentService
) {
super();
this.processExperimentalSettings(Object.keys(this.configurationRegistry.getConfigurationProperties()));
this._register(this.configurationRegistry.onDidUpdateConfiguration(({ properties }) => this.processExperimentalSettings(properties)));
}
private async processExperimentalSettings(properties: string[]): Promise<void> {
const overrides: IStringDictionary<any> = {};
const allProperties = this.configurationRegistry.getConfigurationProperties();
for (const property of properties) {
const schema = allProperties[property];
if (!schema.tags?.includes('experimental')) {
continue;
}
if (this.processedExperimentalSettings.has(property)) {
continue;
}
this.processedExperimentalSettings.add(property);
try {
const value = await this.workbenchAssignmentService.getTreatment(`config.${property}`);
if (!isUndefined(value) && !equals(value, schema.default)) {
overrides[property] = value;
}
} catch (error) {/*ignore */ }
}
if (Object.keys(overrides).length) {
this.configurationRegistry.registerDefaultConfigurations([{ overrides, source: localize('experimental', "Experiments") }]);
}
}
}
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(RegisterConfigurationSchemasContribution, LifecyclePhase.Restored);
workbenchContributionsRegistry.registerWorkbenchContribution(ResetConfigurationDefaultsOverridesCache, LifecyclePhase.Ready);
workbenchContributionsRegistry.registerWorkbenchContribution(UpdateExperimentalSettingsDefaults, LifecyclePhase.Restored);

View file

@ -44,7 +44,8 @@ suite('DefaultConfiguration', () => {
teardown(() => {
configurationRegistry.deregisterConfigurations(configurationRegistry.getConfigurations());
configurationRegistry.deregisterDefaultConfigurations([configurationRegistry.getConfigurationDefaultsOverrides()]);
const configurationDefaultsOverrides = configurationRegistry.getConfigurationDefaultsOverrides();
configurationRegistry.deregisterDefaultConfigurations([...configurationDefaultsOverrides.keys()].map(key => ({ extensionId: configurationDefaultsOverrides.get(key)?.source, overrides: { [key]: configurationDefaultsOverrides.get(key)?.value } })));
});
test('configuration default overrides are read from environment', async () => {
@ -62,6 +63,27 @@ suite('DefaultConfiguration', () => {
assert.deepStrictEqual(actual.getValue('test.configurationDefaultsOverride'), 'overrideValue');
});
test('configuration default overrides are read from cache when model is read before initialize', async () => {
await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' }));
const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService);
assert.deepStrictEqual(testObject.configurationModel.getValue('test.configurationDefaultsOverride'), 'defaultValue');
const actual = await testObject.initialize();
assert.deepStrictEqual(actual.getValue('test.configurationDefaultsOverride'), 'overrideValue');
assert.deepStrictEqual(testObject.configurationModel.getValue('test.configurationDefaultsOverride'), 'overrideValue');
});
test('configuration default overrides are read from cache', async () => {
await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' }));
const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService);
const actual = await testObject.initialize();
assert.deepStrictEqual(actual.getValue('test.configurationDefaultsOverride'), 'overrideValue');
});
test('configuration default overrides read from cache override environment', async () => {
const environmentService = new BrowserWorkbenchEnvironmentService({ logsPath: joinPath(URI.file('tests').with({ scheme: 'vscode-tests' }), 'logs'), workspaceId: '', configurationDefaults: { 'test.configurationDefaultsOverride': 'envOverrideValue' } }, TestProductService);
await configurationCache.write(cacheKey, JSON.stringify({ 'test.configurationDefaultsOverride': 'overrideValue' }));
@ -117,7 +139,7 @@ suite('DefaultConfiguration', () => {
const testObject = new DefaultConfiguration(configurationCache, TestEnvironmentService);
await testObject.initialize();
const promise = Event.toPromise(testObject.onDidChangeConfiguration);
configurationRegistry.registerDefaultConfigurations([{ 'test.configurationDefaultsOverride': 'newoverrideValue' }]);
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'test.configurationDefaultsOverride': 'newoverrideValue' } }]);
await promise;
const actual = JSON.parse(await configurationCache.read(cacheKey));

View file

@ -672,8 +672,10 @@ suite('WorkspaceConfigurationService - Folder', () => {
});
configurationRegistry.registerDefaultConfigurations([{
'[jsonc]': {
'configurationService.folder.languageSetting': 'languageValue'
overrides: {
'[jsonc]': {
'configurationService.folder.languageSetting': 'languageValue'
}
}
}]);
});

View file

@ -5,7 +5,7 @@
import { Event, EventMultiplexer } from 'vs/base/common/event';
import {
ILocalExtension, IGalleryExtension, IExtensionIdentifier, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, TargetPlatform, ExtensionManagementError, ExtensionManagementErrorCode
ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, TargetPlatform, ExtensionManagementError, ExtensionManagementErrorCode
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage } from 'vs/platform/extensions/common/extensions';
@ -28,6 +28,7 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work
import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { isUndefined } from 'vs/base/common/types';
export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService {
@ -292,7 +293,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
}
if (servers.length) {
if (!installOptions) {
if (!installOptions || isUndefined(installOptions.isMachineScoped)) {
const isMachineScoped = await this.hasToFlagExtensionsMachineScoped([gallery]);
installOptions = { isMachineScoped, isBuiltin: false };
}
@ -369,14 +370,17 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
return false;
}
getExtensionsReport(): Promise<IReportedExtension[]> {
getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
if (this.extensionManagementServerService.localExtensionManagementServer) {
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getExtensionsReport();
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getExtensionsControlManifest();
}
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.getExtensionsReport();
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.getExtensionsControlManifest();
}
return Promise.resolve([]);
if (this.extensionManagementServerService.webExtensionManagementServer) {
return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.getExtensionsControlManifest();
}
return Promise.resolve({ malicious: [] });
}
private getServer(extension: ILocalExtension): IExtensionManagementServer | null {

View file

@ -450,7 +450,7 @@ export class LocalProcessExtensionHost implements IExtensionHost {
// using a buffered message protocol here because between now
// and the first time a `then` executes some messages might be lost
// unless we immediately register a listener for `onMessage`.
resolve(new PersistentProtocol(new NodeSocket(this._extensionHostConnection)));
resolve(new PersistentProtocol(new NodeSocket(this._extensionHostConnection, 'renderer-exthost')));
});
// Now that the named pipe listener is installed, start the ext host process

View file

@ -123,10 +123,10 @@ function _createExtHostProtocol(): Promise<PersistentProtocol> {
const initialDataChunk = VSBuffer.wrap(Buffer.from(msg.initialDataChunk, 'base64'));
let socket: NodeSocket | WebSocketNodeSocket;
if (msg.skipWebSocketFrames) {
socket = new NodeSocket(handle);
socket = new NodeSocket(handle, 'extHost-socket');
} else {
const inflateBytes = VSBuffer.wrap(Buffer.from(msg.inflateBytes, 'base64'));
socket = new WebSocketNodeSocket(new NodeSocket(handle), msg.permessageDeflate, inflateBytes, false);
socket = new WebSocketNodeSocket(new NodeSocket(handle, 'extHost-socket'), msg.permessageDeflate, inflateBytes, false);
}
if (protocol) {
// reconnection case
@ -134,9 +134,11 @@ function _createExtHostProtocol(): Promise<PersistentProtocol> {
disconnectRunner2.cancel();
protocol.beginAcceptReconnection(socket, initialDataChunk);
protocol.endAcceptReconnection();
protocol.sendResume();
} else {
clearTimeout(timer);
protocol = new PersistentProtocol(socket, initialDataChunk);
protocol.sendResume();
protocol.onDidDispose(() => onTerminate('renderer disconnected'));
resolve(protocol);
@ -174,7 +176,7 @@ function _createExtHostProtocol(): Promise<PersistentProtocol> {
const socket = net.createConnection(pipeName, () => {
socket.removeListener('error', reject);
resolve(new PersistentProtocol(new NodeSocket(socket)));
resolve(new PersistentProtocol(new NodeSocket(socket, 'extHost-renderer')));
});
socket.once('error', reject);

View file

@ -23,6 +23,7 @@ export interface ILanguageStatus {
readonly severity: Severity;
readonly label: string;
readonly detail: string;
readonly busy: boolean;
readonly source: string;
readonly command: Command | undefined;
readonly accessibilityInfo: IAccessibilityInformation | undefined;

View file

@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ConfigurationScope, EditPresentationTypes, IConfigurationExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry';
import { ConfigurationScope, EditPresentationTypes, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry';
import { EditorResolution, IEditorOptions } from 'vs/platform/editor/common/editor';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
@ -44,7 +44,7 @@ export interface ISettingsGroup {
titleRange: IRange;
sections: ISettingsSection[];
order?: number;
extensionInfo?: IConfigurationExtensionInfo;
extensionInfo?: IExtensionInfo;
}
export interface ISettingsSection {
@ -81,7 +81,7 @@ export interface ISetting {
tags?: string[];
disallowSyncIgnore?: boolean;
restricted?: boolean;
extensionInfo?: IConfigurationExtensionInfo;
extensionInfo?: IExtensionInfo;
validator?: (value: any) => string | null;
enumItemLabels?: string[];
allKeysAreBoolean?: boolean;

View file

@ -15,7 +15,7 @@ import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/mod
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import * as nls from 'vs/nls';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, IConfigurationExtensionInfo, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry';
import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, IExtensionInfo, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
@ -607,7 +607,7 @@ export class DefaultSettings extends Disposable {
return result;
}
private parseSettings(settingsObject: { [path: string]: IConfigurationPropertySchema; }, extensionInfo?: IConfigurationExtensionInfo): ISetting[] {
private parseSettings(settingsObject: { [path: string]: IConfigurationPropertySchema; }, extensionInfo?: IExtensionInfo): ISetting[] {
const result: ISetting[] = [];
for (const key in settingsObject) {
const prop = settingsObject[key];

View file

@ -407,21 +407,15 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
return this.colorThemeRegistry.getThemes();
}
public async getMarketplaceColorThemes(id: string, version: string): Promise<IWorkbenchColorTheme[]> {
const [publisher, name] = id.split('.');
public async getMarketplaceColorThemes(publisher: string, name: string, version: string): Promise<IWorkbenchColorTheme[]> {
const extensionLocation = this.extensionResourceLoaderService.getExtensionGalleryResourceURL({ publisher, name, version }, 'extension');
if (!extensionLocation) {
return [];
}
try {
const manifestContent = await this.extensionResourceLoaderService.readExtensionResource(resources.joinPath(extensionLocation, 'package.json'));
const data: ExtensionData = { extensionPublisher: publisher, extensionId: id, extensionName: name, extensionIsBuiltin: false };
return this.colorThemeRegistry.getMarketplaceThemes(JSON.parse(manifestContent), extensionLocation, data);
} catch (e) {
this.logService.error(`Problem loading themes from marketplace ${e}`);
if (extensionLocation) {
try {
const manifestContent = await this.extensionResourceLoaderService.readExtensionResource(resources.joinPath(extensionLocation, 'package.json'));
return this.colorThemeRegistry.getMarketplaceThemes(JSON.parse(manifestContent), extensionLocation, ExtensionData.fromName(publisher, name));
} catch (e) {
this.logService.error('Problem loading themes from marketplace', e);
}
}
return [];
}
@ -453,6 +447,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
}
try {
await themeData.ensureLoaded(this.extensionResourceLoaderService);
themeData.setCustomizations(this.settings);
return this.applyTheme(themeData, settingsTarget);
} catch (error) {
throw new Error(nls.localize('error.cannotloadtheme', "Unable to load {0}: {1}", themeData.location?.toString(), error.message));
@ -592,13 +587,21 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
return this.onFileIconThemeChange.event;
}
public async setFileIconTheme(iconTheme: string | undefined, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchFileIconTheme> {
public async setFileIconTheme(iconThemeOrId: string | undefined | IWorkbenchFileIconTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchFileIconTheme> {
return this.fileIconThemeSequencer.queue(async () => {
iconTheme = iconTheme || '';
if (iconTheme !== this.currentFileIconTheme.id || !this.currentFileIconTheme.isLoaded) {
if (iconThemeOrId === undefined) {
iconThemeOrId = '';
}
const themeId = types.isString(iconThemeOrId) ? iconThemeOrId : iconThemeOrId.id;
if (themeId !== this.currentFileIconTheme.id || !this.currentFileIconTheme.isLoaded) {
const newThemeData = this.fileIconThemeRegistry.findThemeById(iconTheme) || FileIconThemeData.noIconTheme;
let newThemeData = this.fileIconThemeRegistry.findThemeById(themeId);
if (!newThemeData && iconThemeOrId instanceof FileIconThemeData) {
newThemeData = iconThemeOrId;
}
if (!newThemeData) {
newThemeData = FileIconThemeData.noIconTheme;
}
await newThemeData.ensureLoaded(this.extensionResourceLoaderService);
this.applyAndSetFileIconTheme(newThemeData); // updates this.currentFileIconTheme
@ -616,6 +619,19 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
});
}
public async getMarketplaceFileIconThemes(publisher: string, name: string, version: string): Promise<IWorkbenchFileIconTheme[]> {
const extensionLocation = this.extensionResourceLoaderService.getExtensionGalleryResourceURL({ publisher, name, version }, 'extension');
if (extensionLocation) {
try {
const manifestContent = await this.extensionResourceLoaderService.readExtensionResource(resources.joinPath(extensionLocation, 'package.json'));
return this.fileIconThemeRegistry.getMarketplaceThemes(JSON.parse(manifestContent), extensionLocation, ExtensionData.fromName(publisher, name));
} catch (e) {
this.logService.error('Problem loading themes from marketplace', e);
}
}
return [];
}
private async reloadCurrentFileIconTheme() {
return this.fileIconThemeSequencer.queue(async () => {
await this.currentFileIconTheme.reload(this.extensionResourceLoaderService);
@ -624,15 +640,19 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
}
public async restoreFileIconTheme(): Promise<boolean> {
const settingId = this.settings.fileIconTheme;
const theme = this.fileIconThemeRegistry.findThemeBySettingsId(settingId);
if (theme) {
if (settingId !== this.currentFileIconTheme.settingsId) {
await this.setFileIconTheme(theme.id, undefined);
return this.fileIconThemeSequencer.queue(async () => {
const settingId = this.settings.fileIconTheme;
const theme = this.fileIconThemeRegistry.findThemeBySettingsId(settingId);
if (theme) {
if (settingId !== this.currentFileIconTheme.settingsId) {
await this.setFileIconTheme(theme.id, undefined);
} else if (theme !== this.currentFileIconTheme) {
this.applyAndSetFileIconTheme(theme, true);
}
return true;
}
return true;
}
return false;
return false;
});
}
private applyAndSetFileIconTheme(iconThemeData: FileIconThemeData, silent = false): void {
@ -669,11 +689,20 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
return this.onProductIconThemeChange.event;
}
public async setProductIconTheme(iconTheme: string | undefined, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchProductIconTheme> {
public async setProductIconTheme(iconThemeOrId: string | undefined | IWorkbenchProductIconTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchProductIconTheme> {
return this.productIconThemeSequencer.queue(async () => {
iconTheme = iconTheme || '';
if (iconTheme !== this.currentProductIconTheme.id || !this.currentProductIconTheme.isLoaded) {
const newThemeData = this.productIconThemeRegistry.findThemeById(iconTheme) || ProductIconThemeData.defaultTheme;
if (iconThemeOrId === undefined) {
iconThemeOrId = '';
}
const themeId = types.isString(iconThemeOrId) ? iconThemeOrId : iconThemeOrId.id;
if (themeId !== this.currentProductIconTheme.id || !this.currentProductIconTheme.isLoaded) {
let newThemeData = this.productIconThemeRegistry.findThemeById(themeId);
if (!newThemeData && iconThemeOrId instanceof ProductIconThemeData) {
newThemeData = iconThemeOrId;
}
if (!newThemeData) {
newThemeData = ProductIconThemeData.defaultTheme;
}
await newThemeData.ensureLoaded(this.extensionResourceLoaderService, this.logService);
this.applyAndSetProductIconTheme(newThemeData); // updates this.currentProductIconTheme
@ -690,6 +719,19 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
});
}
public async getMarketplaceProductIconThemes(publisher: string, name: string, version: string): Promise<IWorkbenchProductIconTheme[]> {
const extensionLocation = this.extensionResourceLoaderService.getExtensionGalleryResourceURL({ publisher, name, version }, 'extension');
if (extensionLocation) {
try {
const manifestContent = await this.extensionResourceLoaderService.readExtensionResource(resources.joinPath(extensionLocation, 'package.json'));
return this.productIconThemeRegistry.getMarketplaceThemes(JSON.parse(manifestContent), extensionLocation, ExtensionData.fromName(publisher, name));
} catch (e) {
this.logService.error('Problem loading themes from marketplace', e);
}
}
return [];
}
private async reloadCurrentProductIconTheme() {
return this.productIconThemeSequencer.queue(async () => {
await this.currentProductIconTheme.reload(this.extensionResourceLoaderService, this.logService);
@ -698,15 +740,19 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
}
public async restoreProductIconTheme(): Promise<boolean> {
const settingId = this.settings.productIconTheme;
const theme = this.productIconThemeRegistry.findThemeBySettingsId(settingId);
if (theme) {
if (settingId !== this.currentProductIconTheme.settingsId) {
await this.setProductIconTheme(theme.id, undefined);
return this.productIconThemeSequencer.queue(async () => {
const settingId = this.settings.productIconTheme;
const theme = this.productIconThemeRegistry.findThemeBySettingsId(settingId);
if (theme) {
if (settingId !== this.currentProductIconTheme.settingsId) {
await this.setProductIconTheme(theme.id, undefined);
} else if (theme !== this.currentProductIconTheme) {
this.applyAndSetProductIconTheme(theme, true);
}
return true;
}
return true;
}
return false;
return false;
});
}
private applyAndSetProductIconTheme(iconThemeData: ProductIconThemeData, silent = false): void {

View file

@ -141,13 +141,8 @@ export class ThemeRegistry<T extends IThemeData> {
previousIds[theme.id] = theme;
}
this.extensionThemes.length = 0;
for (let ext of extensions) {
let extensionData: ExtensionData = {
extensionId: ext.description.identifier.value,
extensionPublisher: ext.description.publisher,
extensionName: ext.description.name,
extensionIsBuiltin: ext.description.isBuiltin
};
for (const ext of extensions) {
const extensionData = ExtensionData.fromName(ext.description.publisher, ext.description.name, ext.description.isBuiltin);
this.onThemes(extensionData, ext.description.extensionLocation, ext.value, this.extensionThemes, ext.collector);
}
for (const theme of this.extensionThemes) {

View file

@ -71,18 +71,19 @@ export interface IWorkbenchThemeService extends IThemeService {
setColorTheme(themeId: string | undefined | IWorkbenchColorTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchColorTheme | null>;
getColorTheme(): IWorkbenchColorTheme;
getColorThemes(): Promise<IWorkbenchColorTheme[]>;
getMarketplaceColorThemes(id: string, version: string): Promise<IWorkbenchColorTheme[]>;
getMarketplaceColorThemes(publisher: string, name: string, version: string): Promise<IWorkbenchColorTheme[]>;
onDidColorThemeChange: Event<IWorkbenchColorTheme>;
restoreColorTheme(): void;
setFileIconTheme(iconThemeId: string | undefined, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchFileIconTheme>;
setFileIconTheme(iconThemeId: string | undefined | IWorkbenchFileIconTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchFileIconTheme>;
getFileIconTheme(): IWorkbenchFileIconTheme;
getFileIconThemes(): Promise<IWorkbenchFileIconTheme[]>;
getMarketplaceFileIconThemes(publisher: string, name: string, version: string): Promise<IWorkbenchFileIconTheme[]>;
onDidFileIconThemeChange: Event<IWorkbenchFileIconTheme>;
setProductIconTheme(iconThemeId: string | undefined, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchProductIconTheme>;
setProductIconTheme(iconThemeId: string | undefined | IWorkbenchProductIconTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchProductIconTheme>;
getProductIconTheme(): IWorkbenchProductIconTheme;
getProductIconThemes(): Promise<IWorkbenchProductIconTheme[]>;
getMarketplaceProductIconThemes(publisher: string, name: string, version: string): Promise<IWorkbenchProductIconTheme[]>;
onDidProductIconThemeChange: Event<IWorkbenchProductIconTheme>;
}
@ -199,6 +200,9 @@ export namespace ExtensionData {
}
return undefined;
}
export function fromName(publisher: string, name: string, isBuiltin = false) : ExtensionData {
return { extensionPublisher: publisher, extensionId: `${publisher}.${name}`, extensionName: name, extensionIsBuiltin: isBuiltin };
}
}
export interface IThemeExtensionPoint {

View file

@ -395,7 +395,7 @@ class NewExtensionsInitializer implements IUserDataInitializer {
storeExtensionStorageState(galleryExtension.publisher, galleryExtension.name, extensionToSync.state, this.storageService);
}
this.logService.trace(`Installing extension...`, galleryExtension.identifier.id);
const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: extensionToSync.preRelease } /* pass options to prevent install and sync dialog in web */);
const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: extensionToSync.preRelease } /* set isMachineScoped to prevent install and sync dialog in web */);
if (!preview.disabledExtensions.some(identifier => areSameExtensions(identifier, galleryExtension.identifier))) {
newlyEnabledExtensions.push(local);
}

View file

@ -132,7 +132,7 @@ import { IEditorResolverService } from 'vs/workbench/services/editor/common/edit
import { IWorkingCopyEditorService, WorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService';
import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService';
import { BrowserElevatedFileService } from 'vs/workbench/services/files/browser/elevatedFileService';
import { IDiffComputationResult, IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService';
import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/modes';
import { ResourceMap } from 'vs/base/common/map';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
@ -1866,7 +1866,7 @@ export class TestEditorWorkerService implements IEditorWorkerService {
declare readonly _serviceBrand: undefined;
canComputeUnicodeHighlights(uri: URI): boolean { return false; }
async computedUnicodeHighlights(uri: URI): Promise<IRange[]> { return []; }
async computedUnicodeHighlights(uri: URI): Promise<IUnicodeHighlightsResult> { return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 }; }
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

@ -14,15 +14,59 @@ declare module 'vscode' {
}
interface LanguageStatusItem {
/**
* The identifier of this item.
*/
readonly id: string;
/**
* The short name of this item, like 'Java Language Status', etc.
*/
name: string | undefined;
/**
* A {@link DocumentSelector selector} that defines for what documents
* this item shows.
*/
selector: DocumentSelector;
// todo@jrieken replace with boolean ala needsAttention
severity: LanguageStatusSeverity;
name: string | undefined;
/**
* The text to show for the entry. You can embed icons in the text by leveraging the syntax:
*
* `My text $(icon-name) contains icons like $(icon-name) this one.`
*
* Where the icon-name is taken from the ThemeIcon [icon set](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing), e.g.
* `light-bulb`, `thumbsup`, `zap` etc.
*/
text: string;
/**
* Optional, human-readable details for this item.
*/
detail?: string;
/**
* Controls whether the item is shown as "busy". Defaults to `false`.
*/
busy: boolean;
/**
* A {@linkcode Command command} for this item.
*/
command: Command | undefined;
/**
* Accessibility information used when a screen reader interacts with this item
*/
accessibilityInformation?: AccessibilityInformation;
/**
* Dispose and free associated resources.
*/
dispose(): void;
}

View file

@ -75,10 +75,9 @@ export class Application {
await this.code.waitForElement('.explorer-folders-view');
}
async restart(options: { workspaceOrFolder?: string, extraArgs?: string[] }): Promise<any> {
async restart(options?: { workspaceOrFolder?: string, extraArgs?: string[] }): Promise<any> {
await this.stop();
await new Promise(c => setTimeout(c, 1000));
await this._start(options.workspaceOrFolder, options.extraArgs);
await this._start(options?.workspaceOrFolder, options?.extraArgs);
}
private async _start(workspaceOrFolder = this.workspacePathOrFolder, extraArgs: string[] = []): Promise<any> {
@ -87,15 +86,6 @@ export class Application {
await this.checkWindowReady();
}
async reload(): Promise<any> {
this.code.reload()
.catch(err => null); // ignore the connection drop errors
// needs to be enough to propagate the 'Reload Window' command
await new Promise(c => setTimeout(c, 1500));
await this.checkWindowReady();
}
async stop(): Promise<any> {
if (this._code) {
await this._code.exit();

View file

@ -63,7 +63,7 @@ function getBuildOutPath(root: string): string {
}
}
async function connect(connectDriver: typeof connectElectronDriver, child: cp.ChildProcess | undefined, outPath: string, handlePath: string, logger: Logger): Promise<Code> {
async function connect(connectDriver: typeof connectElectronDriver | typeof connectPlaywrightDriver, child: cp.ChildProcess | undefined, outPath: string, handlePath: string, logger: Logger): Promise<Code> {
let errCount = 0;
while (true) {
@ -79,7 +79,7 @@ async function connect(connectDriver: typeof connectElectronDriver, child: cp.Ch
}
// retry
await new Promise(c => setTimeout(c, 100));
await new Promise(resolve => setTimeout(resolve, 100));
}
}
}
@ -116,14 +116,12 @@ export async function spawn(options: SpawnOptions): Promise<Code> {
const handle = await createDriverHandle();
let child: cp.ChildProcess | undefined;
let connectDriver: typeof connectElectronDriver;
copyExtension(options.extensionsPath, 'vscode-notebook-tests');
if (options.web) {
await launch(options.userDataDir, options.workspacePath, options.codePath, options.extensionsPath, Boolean(options.verbose));
connectDriver = connectPlaywrightDriver.bind(connectPlaywrightDriver, options);
return connect(connectDriver, child, '', handle, options.logger);
return connect(connectPlaywrightDriver.bind(connectPlaywrightDriver, options), child, '', handle, options.logger);
}
const env = { ...process.env };
@ -199,8 +197,7 @@ export async function spawn(options: SpawnOptions): Promise<Code> {
child = cp.spawn(electronPath, args, spawnOptions);
instances.add(child);
child.once('exit', () => instances.delete(child!));
connectDriver = connectElectronDriver;
return connect(connectDriver, child, outPath, handle, options.logger);
return connect(connectElectronDriver, child, outPath, handle, options.logger);
}
async function copyExtension(extensionsPath: string, extId: string): Promise<void> {
@ -290,34 +287,55 @@ export class Code {
await this.driver.dispatchKeybinding(windowId, keybinding);
}
async reload(): Promise<void> {
const windowId = await this.getActiveWindowId();
await this.driver.reloadWindow(windowId);
}
async exit(): Promise<void> {
const exitPromise = this.driver.exitApplication();
return new Promise<void>((resolve, reject) => {
let done = false;
// If we know the `pid`, use that to await the
// process to terminate (desktop).
const pid = this.pid;
if (typeof pid === 'number') {
await (async () => {
while (true) {
try {
process.kill(pid, 0); // throws an exception if the main process doesn't exist anymore.
await new Promise(c => setTimeout(c, 100));
} catch (error) {
return;
}
// Start the exit flow via driver
const exitPromise = this.driver.exitApplication().then(veto => {
if (veto) {
done = true;
reject(new Error('Smoke test exit call resulted in unexpected veto'));
}
})();
}
});
// Otherwise await the exit promise (web).
else {
await exitPromise;
}
// If we know the `pid` of the smoke tested application
// use that as way to detect the exit of the application
const pid = this.pid;
if (typeof pid === 'number') {
(async () => {
let killCounter = 0;
while (!done) {
killCounter++;
if (killCounter > 40) {
done = true;
reject(new Error('Smoke test exit call did not terminate main process after 20s, giving up'));
}
try {
process.kill(pid, 0); // throws an exception if the main process doesn't exist anymore.
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
done = true;
resolve();
}
}
})();
}
// Otherwise await the exit promise (web).
else {
(async () => {
try {
await exitPromise;
resolve();
} catch (error) {
reject(new Error(`Smoke test exit call resulted in error: ${error}`));
}
})();
}
});
}
async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean, retryCount?: number): Promise<string> {

View file

@ -57,9 +57,15 @@ class PlaywrightDriver implements IDriver {
try {
await this._context.tracing.stop({ path: join(logsPath, `playwright-trace-${traceCounter++}.zip`) });
} catch (error) {
console.warn(`Failed to stop playwright tracing.`); // do not fail the build when this fails
console.warn(`Failed to stop playwright tracing: ${error}`);
}
await this._browser.close();
try {
await this._browser.close();
} catch (error) {
console.warn(`Failed to close browser: ${error}`);
}
await teardown();
return false;
@ -207,9 +213,9 @@ export async function launch(userDataDir: string, _workspacePath: string, codeSe
async function teardown(): Promise<void> {
if (server) {
try {
await new Promise<void>((c, e) => kill(server!.pid, err => err ? e(err) : c()));
} catch {
// noop
await new Promise<void>((resolve, reject) => kill(server!.pid, err => err ? reject(err) : resolve()));
} catch (error) {
console.warn(`Error tearing down server: ${error}`);
}
server = undefined;

View file

@ -80,8 +80,8 @@ export class QuickAccess {
}
}
async openFile(fileName: string): Promise<void> {
await this.openQuickAccessAndWait(fileName, true);
async openFile(fileName: string, fileSearch = fileName): Promise<void> {
await this.openQuickAccessAndWait(fileSearch, true);
await this.code.dispatchKeybinding('enter');
await this.editors.waitForActiveTab(fileName);

View file

@ -17,7 +17,7 @@ function toUri(path: string): string {
return `${path}`;
}
async function createWorkspaceFile(workspacePath: string): Promise<string> {
function createWorkspaceFile(workspacePath: string): string {
const workspaceFilePath = path.join(path.dirname(workspacePath), 'smoketest.code-workspace');
const workspace = {
folders: [
@ -39,7 +39,7 @@ async function createWorkspaceFile(workspacePath: string): Promise<string> {
export function setup(opts: minimist.ParsedArgs) {
describe('Multiroot', () => {
beforeSuite(opts, async opts => {
const workspacePath = await createWorkspaceFile(opts.workspacePath);
const workspacePath = createWorkspaceFile(opts.workspacePath);
return { ...opts, workspacePath };
});

View file

@ -1,39 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import minimist = require('minimist');
import { Application } from '../../../../automation';
import { afterSuite, beforeSuite } from '../../utils';
export function setup(opts: minimist.ParsedArgs) {
describe('Dataloss', () => {
beforeSuite(opts);
afterSuite(opts);
it(`verifies that 'hot exit' works for dirty files`, async function () {
const app = this.app as Application;
await app.workbench.editors.newUntitledFile();
const untitled = 'Untitled-1';
const textToTypeInUntitled = 'Hello from Untitled';
await app.workbench.editor.waitForTypeInEditor(untitled, textToTypeInUntitled);
const readmeMd = 'readme.md';
const textToType = 'Hello, Code';
await app.workbench.quickaccess.openFile(readmeMd);
await app.workbench.editor.waitForTypeInEditor(readmeMd, textToType);
await app.reload();
await app.workbench.editors.waitForActiveTab(readmeMd, true);
await app.workbench.editor.waitForEditorContents(readmeMd, c => c.indexOf(textToType) > -1);
await app.workbench.editors.waitForTab(untitled);
await app.workbench.editors.selectTab(untitled);
await app.workbench.editor.waitForEditorContents(untitled, c => c.indexOf(textToTypeInUntitled) > -1);
});
});
}

View file

@ -6,20 +6,80 @@
import { Application, ApplicationOptions, Quality } from '../../../../automation';
import { join } from 'path';
import { ParsedArgs } from 'minimist';
import { afterSuite, timeout } from '../../utils';
import { afterSuite, startApp } from '../../utils';
export function setup(opts: ParsedArgs, testDataPath: string) {
describe('Datamigration', () => {
describe('Data Migration (insiders -> insiders)', () => {
let app: Application | undefined = undefined;
afterSuite(opts, () => app);
it(`verifies opened editors are restored`, async function () {
app = await startApp(opts, this.defaultOptions);
// Open 3 editors and pin 2 of them
await app.workbench.quickaccess.openFile('www');
await app.workbench.quickaccess.runCommand('View: Keep Editor');
await app.workbench.quickaccess.openFile('app.js');
await app.workbench.quickaccess.runCommand('View: Keep Editor');
await app.workbench.editors.newUntitledFile();
await app.restart();
// Verify 3 editors are open
await app.workbench.editors.selectTab('Untitled-1');
await app.workbench.editors.selectTab('app.js');
await app.workbench.editors.selectTab('www');
await app.stop();
app = undefined;
});
it(`verifies that 'hot exit' works for dirty files`, async function () {
app = await startApp(opts, this.defaultOptions);
await app.workbench.editors.newUntitledFile();
const untitled = 'Untitled-1';
const textToTypeInUntitled = 'Hello from Untitled';
await app.workbench.editor.waitForTypeInEditor(untitled, textToTypeInUntitled);
await app.workbench.editors.waitForTab(untitled, true);
const readmeMd = 'readme.md';
const textToType = 'Hello, Code';
await app.workbench.quickaccess.openFile(readmeMd);
await app.workbench.editor.waitForTypeInEditor(readmeMd, textToType);
await app.workbench.editors.waitForTab(readmeMd, true);
await app.restart();
await app.workbench.editors.waitForTab(readmeMd, true);
await app.workbench.quickaccess.openFile(readmeMd);
await app.workbench.editor.waitForEditorContents(readmeMd, c => c.indexOf(textToType) > -1);
await app.workbench.editors.waitForTab(untitled, true);
await app.workbench.quickaccess.openFile(untitled, textToTypeInUntitled);
await app.workbench.editor.waitForEditorContents(untitled, c => c.indexOf(textToTypeInUntitled) > -1);
await app.stop();
app = undefined;
});
});
describe('Data Migration (stable -> insiders)', () => {
let insidersApp: Application | undefined = undefined;
let stableApp: Application | undefined = undefined;
afterSuite(opts, () => insidersApp, async () => stableApp?.stop());
afterSuite(opts, () => insidersApp ?? stableApp, async () => stableApp?.stop());
it(`verifies opened editors are restored`, async function () {
const stableCodePath = opts['stable-build'];
if (!stableCodePath) {
if (!stableCodePath || opts.remote) {
this.skip();
}
@ -69,7 +129,7 @@ export function setup(opts: ParsedArgs, testDataPath: string) {
it(`verifies that 'hot exit' works for dirty files`, async function () {
const stableCodePath = opts['stable-build'];
if (!stableCodePath) {
if (!stableCodePath || opts.remote) {
this.skip();
}
@ -88,13 +148,13 @@ export function setup(opts: ParsedArgs, testDataPath: string) {
const untitled = 'Untitled-1';
const textToTypeInUntitled = 'Hello from Untitled';
await stableApp.workbench.editor.waitForTypeInEditor(untitled, textToTypeInUntitled);
await stableApp.workbench.editors.waitForTab(untitled, true);
const readmeMd = 'readme.md';
const textToType = 'Hello, Code';
await stableApp.workbench.quickaccess.openFile(readmeMd);
await stableApp.workbench.editor.waitForTypeInEditor(readmeMd, textToType);
await timeout(2000); // give time to store the backup before stopping the app
await stableApp.workbench.editors.waitForTab(readmeMd, true);
await stableApp.stop();
stableApp = undefined;
@ -106,11 +166,11 @@ export function setup(opts: ParsedArgs, testDataPath: string) {
await insidersApp.start();
await insidersApp.workbench.editors.waitForTab(readmeMd, true);
await insidersApp.workbench.editors.selectTab(readmeMd);
await insidersApp.workbench.quickaccess.openFile(readmeMd);
await insidersApp.workbench.editor.waitForEditorContents(readmeMd, c => c.indexOf(textToType) > -1);
await insidersApp.workbench.editors.waitForTab(untitled, true);
await insidersApp.workbench.editors.selectTab(untitled);
await insidersApp.workbench.quickaccess.openFile(untitled, textToTypeInUntitled);
await insidersApp.workbench.editor.waitForEditorContents(untitled, c => c.indexOf(textToTypeInUntitled) > -1);
await insidersApp.stop();

View file

@ -4,23 +4,24 @@
*--------------------------------------------------------------------------------------------*/
import minimist = require('minimist');
import * as path from 'path';
import { Application, ApplicationOptions } from '../../../../automation';
import { afterSuite } from '../../utils';
import { join } from 'path';
import { Application } from '../../../../automation';
import { afterSuite, startApp } from '../../utils';
export function setup(opts: minimist.ParsedArgs) {
export function setup(args: minimist.ParsedArgs) {
describe('Launch', () => {
let app: Application;
let app: Application | undefined;
afterSuite(opts, () => app);
afterSuite(args, () => app);
it(`verifies that application launches when user data directory has non-ascii characters`, async function () {
const defaultOptions = this.defaultOptions as ApplicationOptions;
const options: ApplicationOptions = { ...defaultOptions, userDataDir: path.join(defaultOptions.userDataDir, 'abcdø') };
app = new Application(options);
await app.start();
const massagedOptions = { ...this.defaultOptions, userDataDir: join(this.defaultOptions.userDataDir, 'ø') };
app = await startApp(args, massagedOptions);
await app.stop();
app = undefined;
});
});
}

View file

@ -5,20 +5,23 @@
import minimist = require('minimist');
import { Application, Quality } from '../../../../automation';
import { afterSuite, beforeSuite } from '../../utils';
import { afterSuite, startApp } from '../../utils';
export function setup(args: minimist.ParsedArgs) {
export function setup(opts: minimist.ParsedArgs) {
describe('Localization', () => {
beforeSuite(opts);
afterSuite(opts);
let app: Application | undefined = undefined;
afterSuite(args, () => app);
it(`starts with 'DE' locale and verifies title and viewlets text is in German`, async function () {
const app = this.app as Application;
if (app.quality === Quality.Dev || app.remote) {
if (this.defaultOptions.quality === Quality.Dev || this.defaultOptions.remote) {
return this.skip();
}
app = await startApp(args, this.defaultOptions);
await app.workbench.extensions.openExtensionsViewlet();
await app.workbench.extensions.installExtension('ms-ceintl.vscode-language-pack-de', false);
await app.restart({ extraArgs: ['--locale=DE'] });
@ -26,6 +29,9 @@ export function setup(opts: minimist.ParsedArgs) {
const result = await app.workbench.localization.getLocalizedStrings();
const localeInfo = await app.workbench.localization.getLocaleInfo();
await app.stop();
app = undefined;
if (localeInfo.locale === undefined || localeInfo.locale.toLowerCase() !== 'de') {
throw new Error(`The requested locale for VS Code was not German. The received value is: ${localeInfo.locale === undefined ? 'not set' : localeInfo.locale}`);
}

Some files were not shown because too many files have changed in this diff Show more