Compare commits

...

83 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
Martin Aeschlimann 1e473b624f
fix themes. linter errors 2021-11-25 08:40:34 +01:00
Tyler Leonhardt 9ec1ae1d92
silently getting session should return a session (if any) regardless of preference. fixes #137819 2021-11-24 16:40:39 -08:00
Tyler Leonhardt 26fe37ca3d
Revert "Have setting the value trigger the filter. Fixes #137279"
This reverts commit c1c103dee7.
2021-11-24 15:08:35 -08:00
Tyler Leonhardt 60c59ff641
Revert "trigger didChange event when you update inputbox.value. ref c1c103dee729b6f702c1c5272d25b24ea9664d48"
This reverts commit bdc6162b1d.
2021-11-24 15:07:15 -08:00
Sandeep Somavarapu 4947eba7a5
#15756 rename 2021-11-24 23:42:26 +01:00
Sandeep Somavarapu a74b70781e
#15756 sync prereleases 2021-11-24 23:42:26 +01:00
Sandeep Somavarapu f26a89c2c3
#15756 set metadata always and fix hadPreReleaseVersion 2021-11-24 23:42:25 +01:00
Martin Aeschlimann 19a83b8c47
color theme picker: separate quick pick for browsing 2021-11-24 23:42:07 +01:00
rebornix 1f581e229d
Update doc 2021-11-24 14:30:55 -08:00
rebornix 90ca212f5f
Clear Notebook Editor Cache 2021-11-24 13:43:06 -08:00
Raymond Zhao 4a5d3623c0
Polish 2021-11-24 13:12:12 -08:00
Jan Kretschmer bb89815cfb use regex, not Uri.parse, to detect custom scheme 2021-11-24 22:07:31 +01:00
Dirk Baeumer 008a52e298
Fix typo 2021-11-24 21:46:36 +01:00
Dirk Baeumer e346d0f1f9
Add script to prefetch headers for native module compile 2021-11-24 21:45:05 +01:00
Ladislau Szomoru bfad20be9d
Extract remote source provider registry into the vscode.git-base extension (#137656) 2021-11-24 20:48:44 +01:00
Raymond Zhao 483d6f15ed
Trim categories for more cases
Fixes https://github.com/microsoft/vscode/issues/135433
2021-11-24 11:28:52 -08: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
159 changed files with 4097 additions and 1737 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

@ -38,6 +38,7 @@ const compilations = [
'emmet/tsconfig.json',
'extension-editing/tsconfig.json',
'git/tsconfig.json',
'git-base/tsconfig.json',
'github-authentication/tsconfig.json',
'github/tsconfig.json',
'grunt/tsconfig.json',

View file

@ -17,6 +17,7 @@ exports.dirs = [
'extensions/emmet',
'extensions/extension-editing',
'extensions/git',
'extensions/git-base',
'extensions/github',
'extensions/github-authentication',
'extensions/grunt',

37
build/prefetchHeaders.js Normal file
View file

@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const fs = require('fs');
const cp = require('child_process');
function installHeaders(rcFile) {
const lines = fs.readFileSync(rcFile, 'utf8').split(/\r\n?/g);
let disturl, target;
for (const line of lines) {
let match = line.match(/\s*disturl\s*(.*)$/);
if (match !== null && match.length >= 1) {
disturl = match[1];
}
match = line.match(/\s*target\s*(.*)$/);
if (match !== null && match.length >= 1) {
target = match[1];
}
}
if (disturl !== undefined && target !== undefined) {
console.log(`Pre-fetch headers for ${target} from ${disturl}`);
cp.execSync(`node-gyp install --dist-url ${disturl} ${target}`);
}
}
function main() {
installHeaders(path.join(__dirname, '..', '.yarnrc'));
installHeaders(path.join(__dirname, '..', 'remote', '.yarnrc'));
}
if (require.main === module) {
main();
}

View file

@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts'
}
});

View file

@ -8,10 +8,41 @@
"engines": {
"vscode": "0.10.x"
},
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"main": "./out/extension.js",
"icon": "resources/icons/git.png",
"scripts": {
"compile": "gulp compile-extension:git-base",
"watch": "gulp watch-extension:git-base",
"update-grammar": "node ./build/update-grammars.js"
},
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"contributes": {
"commands": [
{
"command": "git-base.api.getRemoteSources",
"title": "%command.api.getRemoteSources%",
"category": "Git Base API"
}
],
"menus": {
"commandPalette": [
{
"command": "git-base.api.getRemoteSources",
"when": "false"
}
]
},
"languages": [
{
"id": "git-commit",
@ -66,5 +97,15 @@
"path": "./syntaxes/ignore.tmLanguage.json"
}
]
},
"dependencies": {
"vscode-nls": "^4.0.0"
},
"devDependencies": {
"@types/node": "14.x"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}

View file

@ -1,4 +1,5 @@
{
"displayName": "Git Base",
"description": "Git static contributions and pickers."
"description": "Git static contributions and pickers.",
"command.api.getRemoteSources": "Get Remote Sources"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, commands } from 'vscode';
import { Model } from '../model';
import { pickRemoteSource } from '../remoteSource';
import { GitBaseExtensionImpl } from './extension';
import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceProvider } from './git-base';
export class ApiImpl implements API {
constructor(private _model: Model) { }
pickRemoteSource(options: PickRemoteSourceOptions): Promise<PickRemoteSourceResult | string | undefined> {
return pickRemoteSource(this._model, options as any);
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
return this._model.registerRemoteSourceProvider(provider);
}
}
export function registerAPICommands(extension: GitBaseExtensionImpl): Disposable {
const disposables: Disposable[] = [];
disposables.push(commands.registerCommand('git-base.api.getRemoteSources', (opts?: PickRemoteSourceOptions) => {
if (!extension.model) {
return;
}
return pickRemoteSource(extension.model, opts as any);
}));
return Disposable.from(...disposables);
}

View file

@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Model } from '../model';
import { GitBaseExtension, API } from './git-base';
import { Event, EventEmitter } from 'vscode';
import { ApiImpl } from './api1';
export class GitBaseExtensionImpl implements GitBaseExtension {
enabled: boolean = false;
private _onDidChangeEnablement = new EventEmitter<boolean>();
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
private _model: Model | undefined = undefined;
set model(model: Model | undefined) {
this._model = model;
const enabled = !!model;
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
this._onDidChangeEnablement.fire(this.enabled);
}
get model(): Model | undefined {
return this._model;
}
constructor(model?: Model) {
if (model) {
this.enabled = true;
this._model = model;
}
}
getAPI(version: number): API {
if (!this._model) {
throw new Error('Git model not found');
}
if (version !== 1) {
throw new Error(`No API version ${version} found.`);
}
return new ApiImpl(this._model);
}
}

View file

@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, ProviderResult, Uri } from 'vscode';
export { ProviderResult } from 'vscode';
export interface API {
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
}
export interface GitBaseExtension {
readonly enabled: boolean;
readonly onDidChangeEnablement: Event<boolean>;
/**
* Returns a specific API version.
*
* Throws error if git-base extension is disabled. You can listed to the
* [GitBaseExtension.onDidChangeEnablement](#GitBaseExtension.onDidChangeEnablement)
* event to know when the extension becomes enabled/disabled.
*
* @param version Version number.
* @returns API instance
*/
getAPI(version: 1): API;
}
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
readonly providerName?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
}
export interface PickRemoteSourceResult {
readonly url: string;
readonly branch?: string;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly url: string | string[];
}
export interface RemoteSourceProvider {
readonly name: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View file

@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { done } from './util';
export function debounce(delay: number): Function {
return decorate((fn, key) => {
const timerKey = `$debounce$${key}`;
return function (this: any, ...args: any[]) {
clearTimeout(this[timerKey]);
this[timerKey] = setTimeout(() => fn.apply(this, args), delay);
};
});
}
export const throttle = decorate(_throttle);
function _throttle<T>(fn: Function, key: string): Function {
const currentKey = `$throttle$current$${key}`;
const nextKey = `$throttle$next$${key}`;
const trigger = function (this: any, ...args: any[]) {
if (this[nextKey]) {
return this[nextKey];
}
if (this[currentKey]) {
this[nextKey] = done(this[currentKey]).then(() => {
this[nextKey] = undefined;
return trigger.apply(this, args);
});
return this[nextKey];
}
this[currentKey] = fn.apply(this, args) as Promise<T>;
const clear = () => this[currentKey] = undefined;
done(this[currentKey]).then(clear, clear);
return this[currentKey];
};
return trigger;
}
function decorate(decorator: (fn: Function, key: string) => Function): Function {
return (_target: any, key: string, descriptor: any) => {
let fnKey: string | null = null;
let fn: Function | null = null;
if (typeof descriptor.value === 'function') {
fnKey = 'value';
fn = descriptor.value;
} else if (typeof descriptor.get === 'function') {
fnKey = 'get';
fn = descriptor.get;
}
if (!fn || !fnKey) {
throw new Error('not supported');
}
descriptor[fnKey] = decorator(fn, key);
};
}

View file

@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext } from 'vscode';
import { registerAPICommands } from './api/api1';
import { GitBaseExtensionImpl } from './api/extension';
import { Model } from './model';
export function activate(context: ExtensionContext): GitBaseExtensionImpl {
const apiImpl = new GitBaseExtensionImpl(new Model());
context.subscriptions.push(registerAPICommands(apiImpl));
return apiImpl;
}

View file

@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EventEmitter, Disposable } from 'vscode';
import { toDisposable } from './util';
import { RemoteSourceProvider } from './api/git-base';
import { IRemoteSourceProviderRegistry } from './remoteProvider';
export class Model implements IRemoteSourceProviderRegistry {
private remoteSourceProviders = new Set<RemoteSourceProvider>();
private _onDidAddRemoteSourceProvider = new EventEmitter<RemoteSourceProvider>();
readonly onDidAddRemoteSourceProvider = this._onDidAddRemoteSourceProvider.event;
private _onDidRemoveRemoteSourceProvider = new EventEmitter<RemoteSourceProvider>();
readonly onDidRemoveRemoteSourceProvider = this._onDidRemoveRemoteSourceProvider.event;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
this.remoteSourceProviders.add(provider);
this._onDidAddRemoteSourceProvider.fire(provider);
return toDisposable(() => {
this.remoteSourceProviders.delete(provider);
this._onDidRemoveRemoteSourceProvider.fire(provider);
});
}
getRemoteProviders(): RemoteSourceProvider[] {
return [...this.remoteSourceProviders.values()];
}
}

View file

@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event } from 'vscode';
import { RemoteSourceProvider } from './api/git';
import { RemoteSourceProvider } from './api/git-base';
export interface IRemoteSourceProviderRegistry {
readonly onDidAddRemoteSourceProvider: Event<RemoteSourceProvider>;
readonly onDidRemoveRemoteSourceProvider: Event<RemoteSourceProvider>;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
getRemoteProviders(): RemoteSourceProvider[];
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
}

View file

@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { QuickPickItem, window, QuickPick } from 'vscode';
import * as nls from 'vscode-nls';
import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base';
import { Model } from './model';
import { throttle, debounce } from './decorators';
const localize = nls.loadMessageBundle();
async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<T>): Promise<T | undefined> {
const result = await new Promise<T | undefined>(c => {
quickpick.onDidAccept(() => c(quickpick.selectedItems[0]));
quickpick.onDidHide(() => c(undefined));
quickpick.show();
});
quickpick.hide();
return result;
}
class RemoteSourceProviderQuickPick {
private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }>;
constructor(private provider: RemoteSourceProvider) {
this.quickpick = window.createQuickPick();
this.quickpick.ignoreFocusOut = true;
if (provider.supportsQuery) {
this.quickpick.placeholder = localize('type to search', "Repository name (type to search)");
this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
} else {
this.quickpick.placeholder = localize('type to filter', "Repository name");
}
}
@debounce(300)
private onDidChangeValue(): void {
this.query();
}
@throttle
private async query(): Promise<void> {
this.quickpick.busy = true;
try {
const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || [];
if (remoteSources.length === 0) {
this.quickpick.items = [{
label: localize('none found', "No remote repositories found."),
alwaysShow: true
}];
} else {
this.quickpick.items = remoteSources.map(remoteSource => ({
label: remoteSource.name,
description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]),
remoteSource,
alwaysShow: true
}));
}
} catch (err) {
this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
console.error(err);
} finally {
this.quickpick.busy = false;
}
}
async pick(): Promise<RemoteSource | undefined> {
this.query();
const result = await getQuickPickResult(this.quickpick);
return result?.remoteSource;
}
}
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>();
quickpick.ignoreFocusOut = true;
if (options.providerName) {
const provider = model.getRemoteProviders()
.filter(provider => provider.name === options.providerName)[0];
if (provider) {
return await pickProviderSource(provider, options);
}
}
const providers = model.getRemoteProviders()
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider }));
quickpick.placeholder = providers.length === 0
? localize('provide url', "Provide repository URL")
: localize('provide url or pick', "Provide repository URL or pick a repository source.");
const updatePicks = (value?: string) => {
if (value) {
quickpick.items = [{
label: options.urlLabel ?? localize('url', "URL"),
description: value,
alwaysShow: true,
url: value
},
...providers];
} else {
quickpick.items = providers;
}
};
quickpick.onDidChangeValue(updatePicks);
updatePicks();
const result = await getQuickPickResult(quickpick);
if (result) {
if (result.url) {
return result.url;
} else if (result.provider) {
return await pickProviderSource(result.provider, options);
}
}
return undefined;
}
async function pickProviderSource(provider: RemoteSourceProvider, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
const quickpick = new RemoteSourceProviderQuickPick(provider);
const remote = await quickpick.pick();
let url: string | undefined;
if (remote) {
if (typeof remote.url === 'string') {
url = remote.url;
} else if (remote.url.length > 0) {
url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
}
}
if (!url || !options.branch) {
return url;
}
if (!provider.getBranches) {
return { url };
}
const branches = await provider.getBranches(url);
if (!branches) {
return { url };
}
const branch = await window.showQuickPick(branches, {
placeHolder: localize('branch name', "Branch name")
});
if (!branch) {
return { url };
}
return { url, branch };
}

View file

@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface IDisposable {
dispose(): void;
}
export function toDisposable(dispose: () => void): IDisposable {
return { dispose };
}
export function done<T>(promise: Promise<T>): Promise<void> {
return promise.then<void>(() => undefined);
}
export namespace Versions {
declare type VersionComparisonResult = -1 | 0 | 1;
export interface Version {
major: number;
minor: number;
patch: number;
pre?: string;
}
export function compare(v1: string | Version, v2: string | Version): VersionComparisonResult {
if (typeof v1 === 'string') {
v1 = fromString(v1);
}
if (typeof v2 === 'string') {
v2 = fromString(v2);
}
if (v1.major > v2.major) { return 1; }
if (v1.major < v2.major) { return -1; }
if (v1.minor > v2.minor) { return 1; }
if (v1.minor < v2.minor) { return -1; }
if (v1.patch > v2.patch) { return 1; }
if (v1.patch < v2.patch) { return -1; }
if (v1.pre === undefined && v2.pre !== undefined) { return 1; }
if (v1.pre !== undefined && v2.pre === undefined) { return -1; }
if (v1.pre !== undefined && v2.pre !== undefined) {
return v1.pre.localeCompare(v2.pre) as VersionComparisonResult;
}
return 0;
}
export function from(major: string | number, minor: string | number, patch?: string | number, pre?: string): Version {
return {
major: typeof major === 'string' ? parseInt(major, 10) : major,
minor: typeof minor === 'string' ? parseInt(minor, 10) : minor,
patch: patch === undefined || patch === null ? 0 : typeof patch === 'string' ? parseInt(patch, 10) : patch,
pre: pre,
};
}
export function fromString(version: string): Version {
const [ver, pre] = version.split('-');
const [major, minor, patch] = ver.split('.');
return from(major, minor, patch, pre);
}
}

View file

@ -0,0 +1,14 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out",
"experimentalDecorators": true,
"typeRoots": [
"./node_modules/@types"
]
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts"
]
}

View file

@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@14.x":
version "14.17.33"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.33.tgz#011ee28e38dc7aee1be032ceadf6332a0ab15b12"
integrity sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==
vscode-nls@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167"
integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==

View file

@ -24,6 +24,9 @@
"*",
"onFileSystem:git"
],
"extensionDependencies": [
"vscode.git-base"
],
"main": "./out/main",
"icon": "resources/icons/git.png",
"scripts": {

View file

@ -5,12 +5,13 @@
import { Model } from '../model';
import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, RemoteSourceProvider, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions } from './git';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands } from 'vscode';
import { mapEvent } from '../util';
import { combinedDisposable, mapEvent } from '../util';
import { toGitUri } from '../uri';
import { pickRemoteSource, PickRemoteSourceOptions } from '../remoteSource';
import { GitExtensionImpl } from './extension';
import { GitBaseApi } from '../git-base';
import { PickRemoteSourceOptions } from './git-base';
class ApiInputBox implements InputBox {
set value(value: string) { this._inputBox.value = value; }
@ -283,7 +284,18 @@ export class ApiImpl implements API {
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
return this._model.registerRemoteSourceProvider(provider);
const disposables: Disposable[] = [];
if (provider.publishRepository) {
disposables.push(this._model.registerRemoteSourcePublisher(provider as RemoteSourcePublisher));
}
disposables.push(GitBaseApi.getAPI().registerRemoteSourceProvider(provider));
return combinedDisposable(disposables);
}
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable {
return this._model.registerRemoteSourcePublisher(publisher);
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
@ -370,11 +382,7 @@ export function registerAPICommands(extension: GitExtensionImpl): Disposable {
}));
disposables.push(commands.registerCommand('git.api.getRemoteSources', (opts?: PickRemoteSourceOptions) => {
if (!extension.model) {
return;
}
return pickRemoteSource(extension.model, opts as any);
return commands.executeCommand('git-base.api.getRemoteSources', opts);
}));
return Disposable.from(...disposables);

60
extensions/git/src/api/git-base.d.ts vendored Normal file
View file

@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, ProviderResult, Uri } from 'vscode';
export { ProviderResult } from 'vscode';
export interface API {
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
}
export interface GitBaseExtension {
readonly enabled: boolean;
readonly onDidChangeEnablement: Event<boolean>;
/**
* Returns a specific API version.
*
* Throws error if git-base extension is disabled. You can listed to the
* [GitBaseExtension.onDidChangeEnablement](#GitBaseExtension.onDidChangeEnablement)
* event to know when the extension becomes enabled/disabled.
*
* @param version Version number.
* @returns API instance
*/
getAPI(version: 1): API;
}
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
readonly providerName?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
}
export interface PickRemoteSourceResult {
readonly url: string;
readonly branch?: string;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly url: string | string[];
}
export interface RemoteSourceProvider {
readonly name: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View file

@ -231,6 +231,12 @@ export interface RemoteSourceProvider {
publishRepository?(repository: Repository): Promise<void>;
}
export interface RemoteSourcePublisher {
readonly name: string;
readonly icon?: string; // codicon name
publishRepository(repository: Repository): Promise<void>;
}
export interface Credentials {
readonly username: string;
readonly password: string;
@ -265,6 +271,7 @@ export interface API {
init(root: Uri): Promise<Repository | null>;
openRepository(root: Uri): Promise<Repository | null>
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;

View file

@ -8,7 +8,7 @@ import * as path from 'path';
import { Command, commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider } from 'vscode';
import TelemetryReporter from 'vscode-extension-telemetry';
import * as nls from 'vscode-nls';
import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider } from './api/git';
import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher } from './api/git';
import { Git, Stash } from './git';
import { Model } from './model';
import { Repository, Resource, ResourceGroupType } from './repository';
@ -392,7 +392,7 @@ export class CommandCenter {
async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean } = {}): Promise<void> {
if (!url || typeof url !== 'string') {
url = await pickRemoteSource(this.model, {
url = await pickRemoteSource({
providerLabel: provider => localize('clonefrom', "Clone from {0}", provider.name),
urlLabel: localize('repourl', "Clone from URL")
});
@ -2215,7 +2215,7 @@ export class CommandCenter {
@command('git.addRemote', { repository: true })
async addRemote(repository: Repository): Promise<string | undefined> {
const url = await pickRemoteSource(this.model, {
const url = await pickRemoteSource({
providerLabel: provider => localize('addfrom', "Add remote from {0}", provider.name),
urlLabel: localize('addFrom', "Add remote from URL")
});
@ -2360,19 +2360,19 @@ export class CommandCenter {
const remotes = repository.remotes;
if (remotes.length === 0) {
const providers = this.model.getRemoteProviders().filter(p => !!p.publishRepository);
const publishers = this.model.getRemoteSourcePublishers();
if (providers.length === 0) {
if (publishers.length === 0) {
window.showWarningMessage(localize('no remotes to publish', "Your repository has no remotes configured to publish to."));
return;
}
let provider: RemoteSourceProvider;
let publisher: RemoteSourcePublisher;
if (providers.length === 1) {
provider = providers[0];
if (publishers.length === 1) {
publisher = publishers[0];
} else {
const picks = providers
const picks = publishers
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('publish to', "Publish to {0}", provider.name), alwaysShow: true, provider }));
const placeHolder = localize('pick provider', "Pick a provider to publish the branch '{0}' to:", branchName);
const choice = await window.showQuickPick(picks, { placeHolder });
@ -2381,10 +2381,10 @@ export class CommandCenter {
return;
}
provider = choice.provider;
publisher = choice.provider;
}
await provider.publishRepository!(new ApiRepository(repository));
await publisher.publishRepository(new ApiRepository(repository));
this.model.firePublishEvent(repository, branchName);
return;

View file

@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { extensions } from 'vscode';
import { API as GitBaseAPI, GitBaseExtension } from './api/git-base';
export class GitBaseApi {
private static _gitBaseApi: GitBaseAPI | undefined;
static getAPI(): GitBaseAPI {
if (!this._gitBaseApi) {
const gitBaseExtension = extensions.getExtension<GitBaseExtension>('vscode.git-base')!.exports;
const onDidChangeGitBaseExtensionEnablement = (enabled: boolean) => {
this._gitBaseApi = enabled ? gitBaseExtension.getAPI(1) : undefined;
};
gitBaseExtension.onDidChangeEnablement(onDidChangeGitBaseExtensionEnablement);
onDidChangeGitBaseExtensionEnablement(gitBaseExtension.enabled);
if (!this._gitBaseApi) {
throw new Error('vscode.git-base extension is not enabled.');
}
}
return this._gitBaseApi;
}
}

View file

@ -12,11 +12,11 @@ import * as path from 'path';
import * as fs from 'fs';
import * as nls from 'vscode-nls';
import { fromGitUri } from './uri';
import { APIState as State, RemoteSourceProvider, CredentialsProvider, PushErrorHandler, PublishEvent } from './api/git';
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher } from './api/git';
import { Askpass } from './askpass';
import { IRemoteSourceProviderRegistry } from './remoteProvider';
import { IPushErrorHandlerRegistry } from './pushError';
import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
const localize = nls.loadMessageBundle();
@ -48,7 +48,7 @@ interface OpenRepository extends Disposable {
repository: Repository;
}
export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRegistry {
export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerRegistry {
private _onDidOpenRepository = new EventEmitter<Repository>();
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
@ -95,13 +95,13 @@ export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRe
return eventToPromise(filterEvent(this.onDidChangeState, s => s === 'initialized')) as Promise<any>;
}
private remoteSourceProviders = new Set<RemoteSourceProvider>();
private remoteSourcePublishers = new Set<RemoteSourcePublisher>();
private _onDidAddRemoteSourceProvider = new EventEmitter<RemoteSourceProvider>();
readonly onDidAddRemoteSourceProvider = this._onDidAddRemoteSourceProvider.event;
private _onDidAddRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
readonly onDidAddRemoteSourcePublisher = this._onDidAddRemoteSourcePublisher.event;
private _onDidRemoveRemoteSourceProvider = new EventEmitter<RemoteSourceProvider>();
readonly onDidRemoveRemoteSourceProvider = this._onDidRemoveRemoteSourceProvider.event;
private _onDidRemoveRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
readonly onDidRemoveRemoteSourcePublisher = this._onDidRemoveRemoteSourcePublisher.event;
private pushErrorHandlers = new Set<PushErrorHandler>();
@ -496,24 +496,24 @@ export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRe
return undefined;
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
this.remoteSourceProviders.add(provider);
this._onDidAddRemoteSourceProvider.fire(provider);
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable {
this.remoteSourcePublishers.add(publisher);
this._onDidAddRemoteSourcePublisher.fire(publisher);
return toDisposable(() => {
this.remoteSourceProviders.delete(provider);
this._onDidRemoveRemoteSourceProvider.fire(provider);
this.remoteSourcePublishers.delete(publisher);
this._onDidRemoveRemoteSourcePublisher.fire(publisher);
});
}
getRemoteSourcePublishers(): RemoteSourcePublisher[] {
return [...this.remoteSourcePublishers.values()];
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
return this.askpass.registerCredentialsProvider(provider);
}
getRemoteProviders(): RemoteSourceProvider[] {
return [...this.remoteSourceProviders.values()];
}
registerPushErrorHandler(handler: PushErrorHandler): Disposable {
this.pushErrorHandlers.add(handler);
return toDisposable(() => this.pushErrorHandlers.delete(handler));

View file

@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event } from 'vscode';
import { RemoteSourcePublisher } from './api/git';
export interface IRemoteSourcePublisherRegistry {
readonly onDidAddRemoteSourcePublisher: Event<RemoteSourcePublisher>;
readonly onDidRemoveRemoteSourcePublisher: Event<RemoteSourcePublisher>;
getRemoteSourcePublishers(): RemoteSourcePublisher[];
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
}

View file

@ -3,180 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { QuickPickItem, window, QuickPick } from 'vscode';
import * as nls from 'vscode-nls';
import { RemoteSourceProvider, RemoteSource } from './api/git';
import { Model } from './model';
import { throttle, debounce } from './decorators';
import { PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base';
import { GitBaseApi } from './git-base';
const localize = nls.loadMessageBundle();
async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<T>): Promise<T | undefined> {
const result = await new Promise<T | undefined>(c => {
quickpick.onDidAccept(() => c(quickpick.selectedItems[0]));
quickpick.onDidHide(() => c(undefined));
quickpick.show();
});
quickpick.hide();
return result;
}
class RemoteSourceProviderQuickPick {
private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }>;
constructor(private provider: RemoteSourceProvider) {
this.quickpick = window.createQuickPick();
this.quickpick.ignoreFocusOut = true;
if (provider.supportsQuery) {
this.quickpick.placeholder = localize('type to search', "Repository name (type to search)");
this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
} else {
this.quickpick.placeholder = localize('type to filter', "Repository name");
}
}
@debounce(300)
private onDidChangeValue(): void {
this.query();
}
@throttle
private async query(): Promise<void> {
this.quickpick.busy = true;
try {
const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || [];
if (remoteSources.length === 0) {
this.quickpick.items = [{
label: localize('none found', "No remote repositories found."),
alwaysShow: true
}];
} else {
this.quickpick.items = remoteSources.map(remoteSource => ({
label: remoteSource.name,
description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]),
remoteSource,
alwaysShow: true
}));
}
} catch (err) {
this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
console.error(err);
} finally {
this.quickpick.busy = false;
}
}
async pick(): Promise<RemoteSource | undefined> {
this.query();
const result = await getQuickPickResult(this.quickpick);
return result?.remoteSource;
}
}
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
readonly providerName?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
}
export interface PickRemoteSourceResult {
readonly url: string;
readonly branch?: string;
}
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>();
quickpick.ignoreFocusOut = true;
if (options.providerName) {
const provider = model.getRemoteProviders()
.filter(provider => provider.name === options.providerName)[0];
if (provider) {
return await pickProviderSource(provider, options);
}
}
const providers = model.getRemoteProviders()
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider }));
quickpick.placeholder = providers.length === 0
? localize('provide url', "Provide repository URL")
: localize('provide url or pick', "Provide repository URL or pick a repository source.");
const updatePicks = (value?: string) => {
if (value) {
quickpick.items = [{
label: options.urlLabel ?? localize('url', "URL"),
description: value,
alwaysShow: true,
url: value
},
...providers];
} else {
quickpick.items = providers;
}
};
quickpick.onDidChangeValue(updatePicks);
updatePicks();
const result = await getQuickPickResult(quickpick);
if (result) {
if (result.url) {
return result.url;
} else if (result.provider) {
return await pickProviderSource(result.provider, options);
}
}
return undefined;
}
async function pickProviderSource(provider: RemoteSourceProvider, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
const quickpick = new RemoteSourceProviderQuickPick(provider);
const remote = await quickpick.pick();
let url: string | undefined;
if (remote) {
if (typeof remote.url === 'string') {
url = remote.url;
} else if (remote.url.length > 0) {
url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
}
}
if (!url || !options.branch) {
return url;
}
if (!provider.getBranches) {
return { url };
}
const branches = await provider.getBranches(url);
if (!branches) {
return { url };
}
const branch = await window.showQuickPick(branches, {
placeHolder: localize('branch name', "Branch name")
});
if (!branch) {
return { url };
}
return { url, branch };
export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;
export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;
export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
return GitBaseApi.getAPI().pickRemoteSource(options);
}

View file

@ -16,9 +16,9 @@ import { toGitUri } from './uri';
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util';
import { IFileWatcher, watch } from './watch';
import { Log, LogLevel } from './log';
import { IRemoteSourceProviderRegistry } from './remoteProvider';
import { IPushErrorHandlerRegistry } from './pushError';
import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
@ -850,8 +850,8 @@ export class Repository implements Disposable {
constructor(
private readonly repository: BaseRepository,
remoteSourceProviderRegistry: IRemoteSourceProviderRegistry,
private pushErrorHandlerRegistry: IPushErrorHandlerRegistry,
remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry,
globalState: Memento,
outputChannel: OutputChannel
) {
@ -959,7 +959,7 @@ export class Repository implements Disposable {
}
}, null, this.disposables);
const statusBar = new StatusBarCommands(this, remoteSourceProviderRegistry);
const statusBar = new StatusBarCommands(this, remoteSourcePublisherRegistry);
this.disposables.push(statusBar);
statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables);
this._sourceControl.statusBarCommands = statusBar.commands;

View file

@ -7,8 +7,8 @@ import { Disposable, Command, EventEmitter, Event, workspace, Uri } from 'vscode
import { Repository, Operation } from './repository';
import { anyEvent, dispose, filterEvent } from './util';
import * as nls from 'vscode-nls';
import { Branch, RemoteSourceProvider } from './api/git';
import { IRemoteSourceProviderRegistry } from './remoteProvider';
import { Branch, RemoteSourcePublisher } from './api/git';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
const localize = nls.loadMessageBundle();
@ -44,7 +44,7 @@ interface SyncStatusBarState {
readonly isSyncRunning: boolean;
readonly hasRemotes: boolean;
readonly HEAD: Branch | undefined;
readonly remoteSourceProviders: RemoteSourceProvider[];
readonly remoteSourcePublishers: RemoteSourcePublisher[];
}
class SyncStatusBar {
@ -60,21 +60,20 @@ class SyncStatusBar {
this._onDidChange.fire();
}
constructor(private repository: Repository, private remoteSourceProviderRegistry: IRemoteSourceProviderRegistry) {
constructor(private repository: Repository, private remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry) {
this._state = {
enabled: true,
isSyncRunning: false,
hasRemotes: false,
HEAD: undefined,
remoteSourceProviders: this.remoteSourceProviderRegistry.getRemoteProviders()
.filter(p => !!p.publishRepository)
remoteSourcePublishers: remoteSourcePublisherRegistry.getRemoteSourcePublishers()
};
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
anyEvent(remoteSourceProviderRegistry.onDidAddRemoteSourceProvider, remoteSourceProviderRegistry.onDidRemoveRemoteSourceProvider)
(this.onDidChangeRemoteSourceProviders, this, this.disposables);
anyEvent(remoteSourcePublisherRegistry.onDidAddRemoteSourcePublisher, remoteSourcePublisherRegistry.onDidRemoveRemoteSourcePublisher)
(this.onDidChangeRemoteSourcePublishers, this, this.disposables);
const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.enableStatusBarSync'));
onEnablementChange(this.updateEnablement, this, this.disposables);
@ -104,11 +103,10 @@ class SyncStatusBar {
};
}
private onDidChangeRemoteSourceProviders(): void {
private onDidChangeRemoteSourcePublishers(): void {
this.state = {
...this.state,
remoteSourceProviders: this.remoteSourceProviderRegistry.getRemoteProviders()
.filter(p => !!p.publishRepository)
remoteSourcePublishers: this.remoteSourcePublisherRegistry.getRemoteSourcePublishers()
};
}
@ -118,12 +116,12 @@ class SyncStatusBar {
}
if (!this.state.hasRemotes) {
if (this.state.remoteSourceProviders.length === 0) {
if (this.state.remoteSourcePublishers.length === 0) {
return;
}
const tooltip = this.state.remoteSourceProviders.length === 1
? localize('publish to', "Publish to {0}", this.state.remoteSourceProviders[0].name)
const tooltip = this.state.remoteSourcePublishers.length === 1
? localize('publish to', "Publish to {0}", this.state.remoteSourcePublishers[0].name)
: localize('publish to...', "Publish to...");
return {
@ -188,8 +186,8 @@ export class StatusBarCommands {
private checkoutStatusBar: CheckoutStatusBar;
private disposables: Disposable[] = [];
constructor(repository: Repository, remoteSourceProviderRegistry: IRemoteSourceProviderRegistry) {
this.syncStatusBar = new SyncStatusBar(repository, remoteSourceProviderRegistry);
constructor(repository: Repository, remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry) {
this.syncStatusBar = new SyncStatusBar(repository, remoteSourcePublisherRegistry);
this.checkoutStatusBar = new CheckoutStatusBar(repository);
this.onDidChange = anyEvent(this.syncStatusBar.onDidChange, this.checkoutStatusBar.onDidChange);
}

View file

@ -16,7 +16,7 @@
"*"
],
"extensionDependencies": [
"vscode.git"
"vscode.git-base"
],
"main": "./out/extension.js",
"capabilities": {
@ -32,6 +32,14 @@
"title": "Publish to GitHub"
}
],
"menus": {
"commandPalette": [
{
"command": "github.publish",
"when": "git-base.gitEnabled"
}
]
},
"configuration": [
{
"title": "GitHub",

View file

@ -3,43 +3,94 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, ExtensionContext, extensions } from 'vscode';
import { commands, Disposable, ExtensionContext, extensions } from 'vscode';
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
import { GitExtension } from './typings/git';
import { registerCommands } from './commands';
import { GithubCredentialProviderManager } from './credentialProvider';
import { dispose, combinedDisposable } from './util';
import { GithubPushErrorHandler } from './pushErrorHandler';
import { GitBaseExtension } from './typings/git-base';
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
export function activate(context: ExtensionContext): void {
context.subscriptions.push(initializeGitBaseExtension());
context.subscriptions.push(initializeGitExtension());
}
function initializeGitBaseExtension(): Disposable {
const disposables = new Set<Disposable>();
context.subscriptions.push(combinedDisposable(disposables));
const init = () => {
const initialize = () => {
try {
const gitAPI = gitExtension.getAPI(1);
const gitBaseAPI = gitBaseExtension.getAPI(1);
disposables.add(registerCommands(gitAPI));
disposables.add(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI)));
disposables.add(new GithubCredentialProviderManager(gitAPI));
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
} catch (err) {
disposables.add(gitBaseAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider()));
}
catch (err) {
console.error('Could not initialize GitHub extension');
console.warn(err);
}
};
const onDidChangeGitExtensionEnablement = (enabled: boolean) => {
const onDidChangeGitBaseExtensionEnablement = (enabled: boolean) => {
if (!enabled) {
dispose(disposables);
disposables.clear();
} else {
init();
initialize();
}
};
const gitBaseExtension = extensions.getExtension<GitBaseExtension>('vscode.git-base')!.exports;
disposables.add(gitBaseExtension.onDidChangeEnablement(onDidChangeGitBaseExtensionEnablement));
onDidChangeGitBaseExtensionEnablement(gitBaseExtension.enabled);
const gitExtension = extensions.getExtension<GitExtension>('vscode.git')!.exports;
context.subscriptions.push(gitExtension.onDidChangeEnablement(onDidChangeGitExtensionEnablement));
onDidChangeGitExtensionEnablement(gitExtension.enabled);
return combinedDisposable(disposables);
}
function initializeGitExtension(): Disposable {
const disposables = new Set<Disposable>();
let gitExtension = extensions.getExtension<GitExtension>('vscode.git');
const initialize = () => {
gitExtension!.activate()
.then(extension => {
const onDidChangeGitExtensionEnablement = (enabled: boolean) => {
if (enabled) {
const gitAPI = extension.getAPI(1);
disposables.add(registerCommands(gitAPI));
disposables.add(new GithubCredentialProviderManager(gitAPI));
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI)));
commands.executeCommand('setContext', 'git-base.gitEnabled', true);
} else {
dispose(disposables);
disposables.clear();
}
};
disposables.add(extension.onDidChangeEnablement(onDidChangeGitExtensionEnablement));
onDidChangeGitExtensionEnablement(extension.enabled);
});
};
if (gitExtension) {
initialize();
} else {
const disposable = extensions.onDidChange(() => {
if (!gitExtension && extensions.getExtension<GitExtension>('vscode.git')) {
gitExtension = extensions.getExtension<GitExtension>('vscode.git');
initialize();
dispose(disposable);
}
});
disposables.add(disposable);
}
return combinedDisposable(disposables);
}

View file

@ -3,10 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { API as GitAPI, RemoteSourceProvider, RemoteSource, Repository } from './typings/git';
import { RemoteSourceProvider, RemoteSource } from './typings/git-base';
import { getOctokit } from './auth';
import { Octokit } from '@octokit/rest';
import { publishRepository } from './publish';
function parse(url: string): { owner: string, repo: string } | undefined {
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\.git/i.exec(url)
@ -30,8 +29,6 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider {
private userReposCache: RemoteSource[] = [];
constructor(private gitAPI: GitAPI) { }
async getRemoteSources(query?: string): Promise<RemoteSource[]> {
const octokit = await getOctokit();
@ -108,8 +105,4 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider {
return branches.sort((a, b) => a === defaultBranch ? -1 : b === defaultBranch ? 1 : 0);
}
publishRepository(repository: Repository): Promise<void> {
return publishRepository(this.gitAPI, repository);
}
}

View file

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { publishRepository } from './publish';
import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git';
export class GithubRemoteSourcePublisher implements RemoteSourcePublisher {
readonly name = 'GitHub';
readonly icon = 'github';
constructor(private gitAPI: GitAPI) { }
publishRepository(repository: Repository): Promise<void> {
return publishRepository(this.gitAPI, repository);
}
}

View file

@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, ProviderResult, Uri } from 'vscode';
export { ProviderResult } from 'vscode';
export interface API {
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
}
export interface GitBaseExtension {
readonly enabled: boolean;
readonly onDidChangeEnablement: Event<boolean>;
/**
* Returns a specific API version.
*
* Throws error if git-base extension is disabled. You can listed to the
* [GitBaseExtension.onDidChangeEnablement](#GitBaseExtension.onDidChangeEnablement)
* event to know when the extension becomes enabled/disabled.
*
* @param version Version number.
* @returns API instance
*/
getAPI(version: 1): API;
}
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
readonly providerName?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
}
export interface PickRemoteSourceResult {
readonly url: string;
readonly branch?: string;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly url: string | string[];
}
export interface RemoteSourceProvider {
readonly name: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View file

@ -216,6 +216,12 @@ export interface RemoteSourceProvider {
publishRepository?(repository: Repository): Promise<void>;
}
export interface RemoteSourcePublisher {
readonly name: string;
readonly icon?: string; // codicon name
publishRepository(repository: Repository): Promise<void>;
}
export interface Credentials {
readonly username: string;
readonly password: string;
@ -243,6 +249,7 @@ export interface API {
getRepository(uri: Uri): Repository | null;
init(root: Uri): Promise<Repository | null>;
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;

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

@ -11,7 +11,6 @@ interface QuickPickExpected {
events: string[];
activeItems: string[][];
selectionItems: string[][];
values: string[];
acceptedItems: {
active: string[][];
selection: string[][];
@ -19,15 +18,6 @@ interface QuickPickExpected {
};
}
interface InputBoxExpected {
events: string[];
values: string[];
accepted: {
values: string[];
dispose: boolean[];
};
}
suite('vscode API - quick input', function () {
teardown(async function () {
@ -45,7 +35,6 @@ suite('vscode API - quick input', function () {
events: ['active', 'active', 'selection', 'accept', 'hide'],
activeItems: [['eins'], ['zwei']],
selectionItems: [['zwei']],
values: [],
acceptedItems: {
active: [['zwei']],
selection: [['zwei']],
@ -72,7 +61,6 @@ suite('vscode API - quick input', function () {
events: ['active', 'selection', 'accept', 'hide'],
activeItems: [['zwei']],
selectionItems: [['zwei']],
values: [],
acceptedItems: {
active: [['zwei']],
selection: [['zwei']],
@ -99,7 +87,6 @@ suite('vscode API - quick input', function () {
events: ['active', 'selection', 'active', 'selection', 'accept', 'hide'],
activeItems: [['eins'], ['zwei']],
selectionItems: [['eins'], ['eins', 'zwei']],
values: [],
acceptedItems: {
active: [['zwei']],
selection: [['eins', 'zwei']],
@ -130,7 +117,6 @@ suite('vscode API - quick input', function () {
events: ['active', 'selection', 'accept', 'selection', 'accept', 'hide'],
activeItems: [['eins']],
selectionItems: [['zwei'], ['drei']],
values: [],
acceptedItems: {
active: [['eins'], ['eins']],
selection: [['zwei'], ['drei']],
@ -156,7 +142,6 @@ suite('vscode API - quick input', function () {
events: ['active', 'selection', 'accept', 'active', 'selection', 'active', 'selection', 'accept', 'hide'],
activeItems: [['eins'], [], ['drei']],
selectionItems: [['eins'], [], ['drei']],
values: [],
acceptedItems: {
active: [['eins'], ['drei']],
selection: [['eins'], ['drei']],
@ -178,40 +163,6 @@ suite('vscode API - quick input', function () {
.catch(err => done(err));
});
// NOTE: This test is currently accepting the wrong behavior of #135971
// so that we can test the fix for #137279.
test('createQuickPick, onDidChangeValue gets triggered', function (_done) {
let done = (err?: any) => {
done = () => { };
_done(err);
};
const quickPick = createQuickPick({
events: ['active', 'active', 'active', 'active', 'value', 'active', 'active', 'value', 'hide'],
activeItems: [['eins'], ['zwei'], [], ['zwei'], [], ['eins']],
selectionItems: [],
values: ['zwei', ''],
acceptedItems: {
active: [],
selection: [],
dispose: []
},
}, (err?: any) => done(err));
quickPick.items = ['eins', 'zwei'].map(label => ({ label }));
quickPick.show();
(async () => {
quickPick.value = 'zwei';
await timeout(async () => {
quickPick.value = '';
await timeout(async () => {
quickPick.hide();
}, 0);
}, 0);
})()
.catch(err => done(err));
});
test('createQuickPick, dispose in onDidHide', function (_done) {
let done = (err?: any) => {
done = () => { };
@ -297,34 +248,6 @@ suite('vscode API - quick input', function () {
quickPick.hide();
await waitForHide(quickPick);
});
test('createInputBox, onDidChangeValue gets triggered', function (_done) {
let done = (err?: any) => {
done = () => { };
_done(err);
};
const quickPick = createInputBox({
events: ['value', 'accept', 'hide'],
values: ['zwei'],
accepted: {
values: ['zwei'],
dispose: [true]
},
}, (err?: any) => done(err));
quickPick.show();
(async () => {
quickPick.value = 'zwei';
await timeout(async () => {
await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
await timeout(async () => {
quickPick.hide();
}, 0);
}, 0);
})()
.catch(err => done(err));
});
});
function createQuickPick(expected: QuickPickExpected, done: (err?: any) => void, record = false) {
@ -393,78 +316,9 @@ function createQuickPick(expected: QuickPickExpected, done: (err?: any) => void,
}
});
quickPick.onDidChangeValue(value => {
if (record) {
console.log('value');
return;
}
try {
eventIndex++;
assert.strictEqual('value', expected.events.shift(), `onDidChangeValue (event ${eventIndex})`);
const expectedValue = expected.values.shift();
assert.deepStrictEqual(value, expectedValue, `onDidChangeValue event value (event ${eventIndex})`);
} catch (err) {
done(err);
}
});
return quickPick;
}
function createInputBox(expected: InputBoxExpected, done: (err?: any) => void, record = false) {
const inputBox = window.createInputBox();
let eventIndex = -1;
inputBox.onDidAccept(() => {
if (record) {
console.log('accept');
return;
}
try {
eventIndex++;
assert.strictEqual('accept', expected.events.shift(), `onDidAccept (event ${eventIndex})`);
const expectedValue = expected.accepted.values.shift();
assert.deepStrictEqual(inputBox.value, expectedValue, `onDidAccept event value (event ${eventIndex})`);
if (expected.accepted.dispose.shift()) {
inputBox.dispose();
}
} catch (err) {
done(err);
}
});
inputBox.onDidHide(() => {
if (record) {
console.log('hide');
done();
return;
}
try {
assert.strictEqual('hide', expected.events.shift());
done();
} catch (err) {
done(err);
}
});
inputBox.onDidChangeValue(value => {
if (record) {
console.log('value');
return;
}
try {
eventIndex++;
assert.strictEqual('value', expected.events.shift(), `onDidChangeValue (event ${eventIndex})`);
const expectedValue = expected.values.shift();
assert.deepStrictEqual(value, expectedValue, `onDidChangeValue event value (event ${eventIndex})`);
} catch (err) {
done(err);
}
});
return inputBox;
}
async function timeout<T>(run: () => Promise<T> | T, ms: number): Promise<T> {
return new Promise<T>(resolve => setTimeout(() => resolve(run()), ms));
}

View file

@ -55,7 +55,8 @@
"minify-vscode-reh-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode-reh-web",
"hygiene": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js hygiene",
"core-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js core-ci",
"extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci"
"extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci",
"prefetch-headers": "node build/prefetchHeaders.js"
},
"dependencies": {
"@microsoft/applicationinsights-web": "^2.6.4",
@ -72,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

@ -490,11 +490,6 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
if (this._value !== value) {
this._value = value || '';
this.update();
// TODO: Remove this duplicate code and have the updating of the input box handle this.
const didFilter = this.ui.list.filter(this.filterValue(this.ui.inputBox.value));
if (didFilter) {
this.trySelectFirst();
}
this.onDidChangeValueEmitter.fire(this._value);
}
}
@ -1061,6 +1056,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
}
class InputBox extends QuickInput implements IInputBox {
private _value = '';
private _valueSelection: Readonly<[number, number]> | undefined;
private valueSelectionUpdated = true;
private _placeholder: string | undefined;
@ -1070,11 +1066,12 @@ class InputBox extends QuickInput implements IInputBox {
private readonly onDidAcceptEmitter = this._register(new Emitter<void>());
get value() {
return this.ui.inputBox.value;
return this._value;
}
set value(value: string) {
this.ui.inputBox.value = value ?? '';
this._value = value || '';
this.update();
}
set valueSelection(valueSelection: Readonly<[number, number]>) {
@ -1121,6 +1118,10 @@ class InputBox extends QuickInput implements IInputBox {
if (!this.visible) {
this.visibleDisposables.add(
this.ui.inputBox.onDidChange(value => {
if (value === this.value) {
return;
}
this._value = value;
this.onDidValueChangeEmitter.fire(value);
}));
this.visibleDisposables.add(this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire()));
@ -1140,6 +1141,9 @@ class InputBox extends QuickInput implements IInputBox {
};
this.ui.setVisibilities(visibilities);
super.update();
if (this.ui.inputBox.value !== this.value) {
this.ui.inputBox.value = this.value;
}
if (this.valueSelectionUpdated) {
this.valueSelectionUpdated = false;
this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] });

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';
@ -22,7 +22,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; isPreReleaseVersion: boolean, hadPreReleaseVersion: boolean, installedTimestamp: number }>;
export type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; isPreReleaseVersion: boolean, preRelease: boolean, installedTimestamp: number }>;
export interface IInstallExtensionTask {
readonly identifier: IExtensionIdentifier;
@ -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';
@ -274,7 +275,7 @@ export interface ILocalExtension extends IExtension {
publisherDisplayName: string | null;
installedTimestamp?: number;
isPreReleaseVersion: boolean;
hadPreReleaseVersion: boolean;
preRelease: boolean;
}
export const enum SortBy {
@ -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

@ -288,7 +288,7 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
installableExtension.metadata.isMachineScoped = this.options.isMachineScoped || existingExtension?.isMachineScoped;
installableExtension.metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin;
installableExtension.metadata.isPreReleaseVersion = this.gallery.properties.isPreReleaseVersion;
installableExtension.metadata.hadPreReleaseVersion = this.gallery.properties.isPreReleaseVersion || existingExtension?.hadPreReleaseVersion;
installableExtension.metadata.preRelease = this.gallery.hasPreReleaseVersion ? this.gallery.properties.isPreReleaseVersion : existingExtension?.preRelease;
try {
const local = await this.installExtension(installableExtension, token);

View file

@ -299,9 +299,7 @@ export class ExtensionsScanner extends Disposable {
const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource;
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
}
this.setMetadata(local, metadata);
return local;
}
} catch (e) {
@ -329,15 +327,15 @@ export class ExtensionsScanner extends Disposable {
}
}
private setMetadata(local: IRelaxedLocalExtension, metadata: Metadata): void {
local.publisherDisplayName = metadata.publisherDisplayName || null;
local.publisherId = metadata.publisherId || null;
local.identifier.uuid = metadata.id;
local.isMachineScoped = !!metadata.isMachineScoped;
local.isPreReleaseVersion = !!metadata.isPreReleaseVersion;
local.hadPreReleaseVersion = !!metadata.hadPreReleaseVersion;
local.isBuiltin = local.type === ExtensionType.System || !!metadata.isBuiltin;
local.installedTimestamp = metadata.installedTimestamp;
private setMetadata(local: IRelaxedLocalExtension, metadata: Metadata | null): void {
local.publisherDisplayName = metadata?.publisherDisplayName || null;
local.publisherId = metadata?.publisherId || null;
local.identifier.uuid = metadata?.id;
local.isMachineScoped = !!metadata?.isMachineScoped;
local.isPreReleaseVersion = !!metadata?.isPreReleaseVersion;
local.preRelease = !!metadata?.preRelease;
local.isBuiltin = local.type === ExtensionType.System || !!metadata?.isBuiltin;
local.installedTimestamp = metadata?.installedTimestamp;
}
private async removeUninstalledExtensions(): Promise<void> {

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

@ -6,6 +6,7 @@
import { IStringDictionary } from 'vs/base/common/collections';
import { deepClone, equals } from 'vs/base/common/objects';
import * as semver from 'vs/base/common/semver/semver';
import { isUndefined } from 'vs/base/common/types';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ISyncExtension, ISyncExtensionWithVersion } from 'vs/platform/userDataSync/common/userDataSync';
@ -89,10 +90,12 @@ export function merge(localExtensions: ISyncExtensionWithVersion[], remoteExtens
const localExtension = localExtensionsMap.get(key);
if (localExtension) {
const remoteExtension = remoteExtensionsMap.get(key)!;
const mergedExtension = updatedInRemote ? remoteExtension : localExtension;
return {
...(updatedInRemote ? remoteExtension : localExtension),
...mergedExtension,
version: remoteExtension.version && semver.gt(remoteExtension.version, localExtension.version) ? localExtension.version : localExtension.version,
state: mergeExtensionState(localExtension, remoteExtension, lastSyncExtensionsMap?.get(key))
state: mergeExtensionState(localExtension, remoteExtension, lastSyncExtensionsMap?.get(key)),
preRelease: isUndefined(mergedExtension.preRelease) /* from older client*/ ? localExtension.preRelease : mergedExtension.preRelease
};
}
@ -210,6 +213,7 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
const toExtension = to.get(key);
if (!toExtension
|| fromExtension.disabled !== toExtension.disabled
|| fromExtension.preRelease !== toExtension.preRelease
|| !isSameExtensionState(fromExtension.state, toExtension.state)
|| (checkVersionProperty && fromExtension.version !== toExtension.version)
|| (checkInstalledProperty && fromExtension.installed !== toExtension.installed)
@ -307,6 +311,7 @@ function massageOutgoingExtension<T extends ISyncExtension>(extension: T, key: s
id: extension.identifier.id,
uuid: key.startsWith('uuid:') ? key.substring('uuid:'.length) : undefined
},
preRelease: !!extension.preRelease /* set it always so that to differentiate with older clients */
};
if (extension.version) {
massagedExtension.version = extension.version;

View file

@ -364,21 +364,24 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
if (e.state && installedExtension.manifest.version === e.version) {
this.updateExtensionState(e.state, installedExtension.manifest.publisher, installedExtension.manifest.name, installedExtension.manifest.version);
}
if (e.disabled) {
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
await this.extensionEnablementService.disableExtension(e.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id);
await this.extensionEnablementService.enableExtension(e.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id);
const isDisabled = this.extensionEnablementService.getDisabledExtensions().some(disabledExtension => areSameExtensions(disabledExtension, e.identifier));
if (isDisabled !== !!e.disabled) {
if (e.disabled) {
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
await this.extensionEnablementService.disableExtension(e.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id);
await this.extensionEnablementService.enableExtension(e.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id);
}
}
removeFromSkipped.push(e.identifier);
return;
}
// User Extension Sync: Install/Update, Enablement & State
const extension = (await this.extensionGalleryService.getExtensions([e.identifier], CancellationToken.None))[0];
const extension = (await this.extensionGalleryService.getExtensions([e.identifier], !!e.preRelease, CancellationToken.None))[0];
/* Update extension state only if
* extension is installed and version is same as synced version or
@ -395,21 +398,25 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
if (extension) {
try {
if (e.disabled) {
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id, extension.version);
await this.extensionEnablementService.disableExtension(extension.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id, extension.version);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id, extension.version);
await this.extensionEnablementService.enableExtension(extension.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id, extension.version);
const isDisabled = this.extensionEnablementService.getDisabledExtensions().some(disabledExtension => areSameExtensions(disabledExtension, e.identifier));
if (isDisabled !== !!e.disabled) {
if (e.disabled) {
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id, extension.version);
await this.extensionEnablementService.disableExtension(extension.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Disabled extension`, e.identifier.id, extension.version);
} else {
this.logService.trace(`${this.syncResourceLogLabel}: Enabling extension...`, e.identifier.id, extension.version);
await this.extensionEnablementService.enableExtension(extension.identifier);
this.logService.info(`${this.syncResourceLogLabel}: Enabled extension`, e.identifier.id, extension.version);
}
}
// Install only if the extension does not exist
if (!installedExtension) {
if (!installedExtension // Install if the extension does not exist
|| installedExtension.preRelease !== e.preRelease // Install if the extension pre-release preference has changed
) {
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 } /* 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 {
@ -465,8 +472,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
private getLocalExtensions(installedExtensions: ILocalExtension[]): ISyncExtensionWithVersion[] {
const disabledExtensions = this.extensionEnablementService.getDisabledExtensions();
return installedExtensions
.map(({ identifier, isBuiltin, manifest }) => {
const syncExntesion: ISyncExtensionWithVersion = { identifier, version: manifest.version };
.map(({ identifier, isBuiltin, manifest, preRelease }) => {
const syncExntesion: ISyncExtensionWithVersion = { identifier, version: manifest.version, preRelease };
if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
syncExntesion.disabled = true;
}

View file

@ -301,6 +301,7 @@ export namespace UserDataSyncError {
export interface ISyncExtension {
identifier: IExtensionIdentifier;
preRelease?: boolean;
version?: string;
disabled?: boolean;
installed?: boolean;

View file

@ -117,15 +117,15 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, expected);
@ -141,14 +141,14 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, null, [], ['a']);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, expected);
@ -170,7 +170,7 @@ suite('ExtensionsMerge', () => {
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, [{ id: 'a', uuid: 'a' }, { id: 'd', uuid: 'd' }]);
assert.deepStrictEqual(actual.local.updated, []);
assert.strictEqual(actual.remote, null);
@ -193,9 +193,9 @@ suite('ExtensionsMerge', () => {
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, [{ id: 'a', uuid: 'a' }]);
assert.deepStrictEqual(actual.local.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true, version: '1.0.0', preRelease: false }]);
assert.strictEqual(actual.remote, null);
});
@ -215,7 +215,7 @@ suite('ExtensionsMerge', () => {
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a']);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepStrictEqual(actual.local.updated, []);
assert.strictEqual(actual.remote, null);
@ -239,7 +239,7 @@ suite('ExtensionsMerge', () => {
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false }, { identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepStrictEqual(actual.local.updated, []);
assert.strictEqual(actual.remote, null);
@ -263,7 +263,7 @@ suite('ExtensionsMerge', () => {
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['b']);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepStrictEqual(actual.local.updated, []);
assert.strictEqual(actual.remote, null);
@ -282,13 +282,17 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, localExtensions);
assert.deepStrictEqual(actual.remote?.all, expected);
});
test('merge local and remote extensions when local is moved forwarded with disabled extensions', () => {
@ -305,13 +309,18 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, disabled: true, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, localExtensions);
assert.deepStrictEqual(actual.remote?.all, expected);
});
test('merge local and remote extensions when local is moved forwarded with ignored settings', () => {
@ -334,7 +343,7 @@ suite('ExtensionsMerge', () => {
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, [
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
]);
});
@ -355,9 +364,9 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
@ -385,8 +394,8 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['c']);
@ -413,14 +422,14 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, [{ id: 'a', uuid: 'a' }]);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, expected);
@ -442,9 +451,9 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a', 'e']);
@ -473,14 +482,14 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, expected);
@ -504,9 +513,9 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'e', uuid: 'e' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['e']);
@ -528,15 +537,15 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'A', uuid: 'a' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0' },
{ identifier: { id: 'A', uuid: 'a' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'b', uuid: 'b' }, installed: true, version: '1.0.0', preRelease: false },
{ identifier: { id: 'c', uuid: 'c' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0' }]);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'd', uuid: 'd' }, installed: true, version: '1.0.0', preRelease: false }]);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, expected);
@ -566,13 +575,16 @@ suite('ExtensionsMerge', () => {
const remoteExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, localExtensions);
assert.deepStrictEqual(actual.remote?.all, expected);
});
test('merge when an extension is not an installed extension remotely and does not exist locally', () => {
@ -600,7 +612,7 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'a', uuid: 'a' }, installed: true, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true, version: '1.0.0' },
{ identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []);
@ -623,7 +635,9 @@ suite('ExtensionsMerge', () => {
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, remoteExtensions);
assert.deepStrictEqual(actual.local.updated, [
{ identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true, version: '1.0.0', preRelease: false },
]);
assert.deepStrictEqual(actual.remote, null);
});
@ -635,8 +649,8 @@ suite('ExtensionsMerge', () => {
{ identifier: { id: 'b', uuid: 'b' }, version: '1.0.0' },
];
const expected: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'b', uuid: 'b' }, version: '1.0.0' },
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0' },
{ identifier: { id: 'b', uuid: 'b' }, version: '1.0.0', preRelease: false },
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
@ -647,4 +661,128 @@ suite('ExtensionsMerge', () => {
assert.deepStrictEqual(actual.remote?.all, expected);
});
test('merge: remote extension with prerelease is added', () => {
const localExtensions: ISyncExtensionWithVersion[] = [];
const remoteExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepStrictEqual(actual.local.added, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true }]);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote, null);
});
test('merge: local extension with prerelease is added', () => {
const localExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true },
];
const remoteExtensions: ISyncExtensionWithVersion[] = [];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true }]);
});
test('merge: remote extension with prerelease is added when local extension without prerelease is added', () => {
const localExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true },
];
const remoteExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true }]);
assert.deepStrictEqual(actual.remote, null);
});
test('merge: remote extension without prerelease is added when local extension with prerelease is added', () => {
const localExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true },
];
const remoteExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true }]);
assert.deepStrictEqual(actual.remote?.all, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true }]);
});
test('merge: remote extension is changed to prerelease', () => {
const localExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true },
];
const remoteExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true },
];
const actual = merge(localExtensions, remoteExtensions, localExtensions, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true }]);
assert.deepStrictEqual(actual.remote, null);
});
test('merge: remote extension is changed to release', () => {
const localExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true },
];
const remoteExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: false },
];
const actual = merge(localExtensions, remoteExtensions, localExtensions, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: false }]);
assert.deepStrictEqual(actual.remote, null);
});
test('merge: local extension is changed to prerelease', () => {
const localExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true },
];
const remoteExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true },
];
const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true }]);
});
test('merge: local extension is changed to release', () => {
const localExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: false },
];
const remoteExtensions: ISyncExtensionWithVersion[] = [
{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: true },
];
const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []);
assert.deepStrictEqual(actual.local.added, []);
assert.deepStrictEqual(actual.local.removed, []);
assert.deepStrictEqual(actual.local.updated, []);
assert.deepStrictEqual(actual.remote?.all, [{ identifier: { id: 'a', uuid: 'a' }, version: '1.0.0', installed: true, preRelease: false }]);
});
});

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

@ -207,6 +207,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<modes.AuthenticationSession | undefined> {
const sessions = await this.authenticationService.getSessions(providerId, scopes, true);
const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId);
// Error cases
if (options.forceNewSession && !sessions.length) {
@ -224,7 +225,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
// Check if the sessions we have are valid
if (!options.forceNewSession && sessions.length) {
if (this.authenticationService.supportsMultipleAccounts(providerId)) {
if (supportsMultipleAccounts) {
if (options.clearSessionPreference) {
this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL);
} else {
@ -251,18 +252,20 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
throw new Error('User did not consent to login.');
}
const session = sessions?.length && !options.forceNewSession
const session = sessions?.length && !options.forceNewSession && supportsMultipleAccounts
? await this.authenticationService.selectSession(providerId, extensionId, extensionName, scopes, sessions)
: await this.authenticationService.createSession(providerId, scopes, true);
await this.setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
return session;
}
// passive flows
if (!options.silent) {
// passive flows (silent or default)
const validSession = sessions.find(s => this.authenticationService.isAccessAllowed(providerId, s.account.label, extensionId));
if (!options.silent && !validSession) {
await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName);
}
return undefined;
return validSession;
}
async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<modes.AuthenticationSession | undefined> {

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);

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