Compare commits
83 commits
lramos15/m
...
main
Author | SHA1 | Date | |
---|---|---|---|
7dfcd74e63 | |||
fd83e2135c | |||
438dc2f8b5 | |||
df4b5d6d04 | |||
e2adc711f3 | |||
40c9c3f677 | |||
319529dc9d | |||
153a028f3b | |||
212deea1cc | |||
13ff6baa3f | |||
5cb1766018 | |||
49fc9c109e | |||
39c6132b2c | |||
d673cdb0ec | |||
c67fd6e5cd | |||
22a1d0b1d2 | |||
f1455eabed | |||
6b2aa3abfd | |||
bc75bda008 | |||
a8b571c9f3 | |||
5ce5e6cc02 | |||
8a305e17d5 | |||
ab8b0b914a | |||
75d09de705 | |||
42ec6e7924 | |||
8d250e99e7 | |||
bab15fcbb0 | |||
420c749ca8 | |||
bfd3bee273 | |||
1a3dce1b0c | |||
eca6f7ed24 | |||
a11814c57a | |||
01d1ea52e6 | |||
d18d093403 | |||
480888c7ce | |||
a45c7f09cc | |||
503a9bcd16 | |||
4aad18d229 | |||
61eda668bd | |||
8dbd9d0ee6 | |||
eecd0038f6 | |||
be87ebcd0d | |||
1c1df5532d | |||
3fb9624b29 | |||
118d34cbe7 | |||
60d21965c7 | |||
ab394ee788 | |||
dd19b1d50b | |||
7b3474abff | |||
10d3e93db5 | |||
a3dce400d6 | |||
2b6fd1df46 | |||
944d343cc2 | |||
f853123bff | |||
6f2239307b | |||
2f8fb0b32e | |||
babe6a6a94 | |||
5e5bb86a25 | |||
131f9fa97c | |||
20d492a0a0 | |||
228ac5b3c1 | |||
7ca8fbe2d3 | |||
f04acdb07e | |||
648e355c05 | |||
1e473b624f | |||
9ec1ae1d92 | |||
26fe37ca3d | |||
60c59ff641 | |||
4947eba7a5 | |||
a74b70781e | |||
f26a89c2c3 | |||
19a83b8c47 | |||
1f581e229d | |||
90ca212f5f | |||
4a5d3623c0 | |||
bb89815cfb | |||
008a52e298 | |||
e346d0f1f9 | |||
bfad20be9d | |||
483d6f15ed | |||
602c83b7bc | |||
8779aaf2ae | |||
b074018c3e |
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
37
build/prefetchHeaders.js
Normal 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();
|
||||
}
|
17
extensions/git-base/extension.webpack.config.js
Normal file
17
extensions/git-base/extension.webpack.config.js
Normal 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'
|
||||
}
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
BIN
extensions/git-base/resources/icons/git.png
Normal file
BIN
extensions/git-base/resources/icons/git.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
37
extensions/git-base/src/api/api1.ts
Normal file
37
extensions/git-base/src/api/api1.ts
Normal 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);
|
||||
}
|
55
extensions/git-base/src/api/extension.ts
Normal file
55
extensions/git-base/src/api/extension.ts
Normal 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);
|
||||
}
|
||||
}
|
60
extensions/git-base/src/api/git-base.d.ts
vendored
Normal file
60
extensions/git-base/src/api/git-base.d.ts
vendored
Normal 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[]>;
|
||||
}
|
69
extensions/git-base/src/decorators.ts
Normal file
69
extensions/git-base/src/decorators.ts
Normal 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);
|
||||
};
|
||||
}
|
16
extensions/git-base/src/extension.ts
Normal file
16
extensions/git-base/src/extension.ts
Normal 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;
|
||||
}
|
34
extensions/git-base/src/model.ts
Normal file
34
extensions/git-base/src/model.ts
Normal 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()];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
170
extensions/git-base/src/remoteSource.ts
Normal file
170
extensions/git-base/src/remoteSource.ts
Normal 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 };
|
||||
}
|
69
extensions/git-base/src/util.ts
Normal file
69
extensions/git-base/src/util.ts
Normal 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);
|
||||
}
|
||||
}
|
14
extensions/git-base/tsconfig.json
Normal file
14
extensions/git-base/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
13
extensions/git-base/yarn.lock
Normal file
13
extensions/git-base/yarn.lock
Normal 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==
|
|
@ -24,6 +24,9 @@
|
|||
"*",
|
||||
"onFileSystem:git"
|
||||
],
|
||||
"extensionDependencies": [
|
||||
"vscode.git-base"
|
||||
],
|
||||
"main": "./out/main",
|
||||
"icon": "resources/icons/git.png",
|
||||
"scripts": {
|
||||
|
|
|
@ -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
60
extensions/git/src/api/git-base.d.ts
vendored
Normal 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[]>;
|
||||
}
|
7
extensions/git/src/api/git.d.ts
vendored
7
extensions/git/src/api/git.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
30
extensions/git/src/git-base.ts
Normal file
30
extensions/git/src/git-base.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
15
extensions/git/src/remotePublisher.ts
Normal file
15
extensions/git/src/remotePublisher.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
18
extensions/github/src/remoteSourcePublisher.ts
Normal file
18
extensions/github/src/remoteSourcePublisher.ts
Normal 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);
|
||||
}
|
||||
}
|
60
extensions/github/src/typings/git-base.d.ts
vendored
Normal file
60
extensions/github/src/typings/git-base.d.ts
vendored
Normal 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[]>;
|
||||
}
|
7
extensions/github/src/typings/git.d.ts
vendored
7
extensions/github/src/typings/git.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
// })();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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] });
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -899,6 +899,8 @@ export interface ICodeEditor extends editorCommon.IEditor {
|
|||
* @internal
|
||||
*/
|
||||
hasModel(): this is IActiveCodeEditor;
|
||||
|
||||
setBanner(bannerDomNode: HTMLElement | null, height: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>({
|
||||
|
|
|
@ -162,6 +162,7 @@ export interface IConfiguration extends IDisposable {
|
|||
observeReferenceElement(dimension?: IDimension): void;
|
||||
updatePixelRatio(): void;
|
||||
setIsDominatedByLongLines(isDominatedByLongLines: boolean): void;
|
||||
reserveHeight(height: number): void;
|
||||
}
|
||||
|
||||
// --- view
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
155
src/vs/editor/contrib/unicodeHighlighter/bannerController.ts
Normal file
155
src/vs/editor/contrib/unicodeHighlighter/bannerController.ts
Normal 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;
|
||||
}
|
|
@ -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
8
src/vs/monaco.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.") },
|
||||
|
||||
|
|
|
@ -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']) {
|
||||
|
|
|
@ -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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'); }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -301,6 +301,7 @@ export namespace UserDataSyncError {
|
|||
|
||||
export interface ISyncExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
preRelease?: boolean;
|
||||
version?: string;
|
||||
disabled?: boolean;
|
||||
installed?: boolean;
|
||||
|
|
|
@ -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 }]);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue