Merge branch 'master' into chrisdias/solongsofar

This commit is contained in:
Chris Dias
2021-01-20 20:58:31 +00:00
committed by GitHub
153 changed files with 3186 additions and 2098 deletions
+69 -15
View File
@@ -5,7 +5,10 @@
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "jsdoc"],
"plugins": [
"@typescript-eslint",
"jsdoc"
],
"rules": {
"constructor-super": "warn",
"curly": "warn",
@@ -43,7 +46,9 @@
"warn",
{
"selector": "class",
"format": ["PascalCase"]
"format": [
"PascalCase"
]
}
],
"code-no-unused-expressions": [
@@ -60,11 +65,26 @@
"warn",
{
"common": [],
"node": ["common"],
"browser": ["common"],
"electron-sandbox": ["common", "browser"],
"electron-browser": ["common", "browser", "node", "electron-sandbox"],
"electron-main": ["common", "node"]
"node": [
"common"
],
"browser": [
"common"
],
"electron-sandbox": [
"common",
"browser"
],
"electron-browser": [
"common",
"browser",
"node",
"electron-sandbox"
],
"electron-main": [
"common",
"node"
]
}
],
"code-import-patterns": [
@@ -74,7 +94,10 @@
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
{
"target": "**/vs/base/common/**",
"restrictions": ["vs/nls", "**/vs/base/common/**"]
"restrictions": [
"vs/nls",
"**/vs/base/common/**"
]
},
{
"target": "**/vs/base/test/common/**",
@@ -432,7 +455,11 @@
},
{
"target": "**/vs/workbench/api/worker/**",
"restrictions": ["vscode", "vs/nls", "**/vs/**/{common,worker}/**"]
"restrictions": [
"vscode",
"vs/nls",
"**/vs/**/{common,worker}/**"
]
},
{
"target": "**/vs/workbench/electron-sandbox/**",
@@ -878,7 +905,13 @@
},
{
"target": "**/api/**.test.ts",
"restrictions": ["**/vs/**", "assert", "sinon", "crypto", "vscode"]
"restrictions": [
"**/vs/**",
"assert",
"sinon",
"crypto",
"vscode"
]
},
{
"target": "**/{node,electron-browser,electron-main}/**/*.test.ts",
@@ -903,28 +936,46 @@
},
{
"target": "**/**.test.ts",
"restrictions": ["**/vs/**", "assert", "sinon", "crypto", "xterm*"]
"restrictions": [
"**/vs/**",
"assert",
"sinon",
"crypto",
"xterm*"
]
},
{
"target": "**/test/**",
"restrictions": ["**/vs/**", "assert", "sinon", "crypto", "xterm*"]
"restrictions": [
"**/vs/**",
"assert",
"sinon",
"crypto",
"xterm*"
]
}
]
},
"overrides": [
{
"files": ["*.js"],
"files": [
"*.js"
],
"rules": {
"jsdoc/no-types": "off"
}
},
{
"files": ["**/vscode.d.ts", "**/vscode.proposed.d.ts"],
"files": [
"**/vscode.d.ts",
"**/vscode.proposed.d.ts"
],
"rules": {
"vscode-dts-create-func": "warn",
"vscode-dts-literal-or-types": "warn",
"vscode-dts-interface-naming": "warn",
"vscode-dts-cancellation": "warn",
"vscode-dts-use-thenable": "warn",
"vscode-dts-provider-naming": [
"warn",
{
@@ -940,7 +991,10 @@
"vscode-dts-event-naming": [
"warn",
{
"allowed": ["onCancellationRequested", "event"],
"allowed": [
"onCancellationRequested",
"event"
],
"verbs": [
"accept",
"change",
@@ -2,6 +2,7 @@ pool:
vmImage: "Ubuntu-16.04"
trigger: none
pr: none
steps:
- task: NodeTool@0
@@ -0,0 +1,24 @@
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
module.exports = new class ApiEventNaming {
constructor() {
this.meta = {
messages: {
usage: 'Use the Thenable-type instead of the Promise type',
}
};
}
create(context) {
return {
['TSTypeAnnotation TSTypeReference Identifier[name="Promise"]']: (node) => {
context.report({
node,
messageId: 'usage',
});
}
};
}
};
@@ -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 * as eslint from 'eslint';
export = new class ApiEventNaming implements eslint.Rule.RuleModule {
readonly meta: eslint.Rule.RuleMetaData = {
messages: {
usage: 'Use the Thenable-type instead of the Promise type',
}
};
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
return {
['TSTypeAnnotation TSTypeReference Identifier[name="Promise"]']: (node: any) => {
context.report({
node,
messageId: 'usage',
});
}
};
}
};
+1
View File
@@ -194,6 +194,7 @@ function fromMarketplace(extensionName, version, metadata) {
exports.fromMarketplace = fromMarketplace;
const excludedExtensions = [
'vscode-api-tests',
'vscode-colorize-tests',
'vscode-test-resolver',
'ms-vscode.node-debug',
'ms-vscode.node-debug2',
+34
View File
@@ -28,7 +28,41 @@ if (!/yarn[\w-.]*\.js$|yarnpkg$/.test(process.env['npm_execpath'])) {
err = true;
}
if (process.platform === 'win32') {
if (!hasSupportedVisualStudioVersion()) {
console.error('\033[1;31m*** Invalid C/C++ Compiler Toolchain. Please check https://github.com/microsoft/vscode/wiki/How-to-Contribute.\033[0;0m');
err = true;
}
}
if (err) {
console.error('');
process.exit(1);
}
function hasSupportedVisualStudioVersion() {
const fs = require('fs');
const path = require('path');
// Translated over from
// https://source.chromium.org/chromium/chromium/src/+/master:build/vs_toolchain.py;l=140-175
const supportedVersions = ['2019', '2017'];
const availableVersions = [];
for (const version of supportedVersions) {
let vsPath = process.env[`vs${version}_install`];
if (vsPath && fs.existsSync(vsPath)) {
availableVersions.push(version);
break;
}
const programFiles86Path = process.env['ProgramFiles(x86)'];
if (programFiles86Path) {
vsPath = `${programFiles86Path}/Microsoft Visual Studio/${version}`;
const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools'];
if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) {
availableVersions.push(version);
break;
}
}
}
return availableVersions.length;
}
+1 -1
View File
@@ -467,7 +467,7 @@ export class Git {
try {
const networkPath = await new Promise<string | undefined>(resolve =>
realpath.native(`${letter}:`, { encoding: 'utf8' }, (err, resolvedPath) =>
realpath.native(`${letter}:\\`, { encoding: 'utf8' }, (err, resolvedPath) =>
resolve(err !== null ? undefined : resolvedPath),
),
);
@@ -34,7 +34,7 @@ export class Keychain {
constructor(private context: vscode.ExtensionContext) { }
async setToken(token: string): Promise<void> {
try {
return await this.context.secrets.set(SERVICE_ID, token);
return await this.context.secrets.store(SERVICE_ID, token);
} catch (e) {
// Ignore
Logger.error(`Setting token failed: ${e}`);
@@ -15,6 +15,6 @@ export class RenderDocument implements Command {
) { }
public async execute(document: SkinnyTextDocument | string): Promise<string> {
return this.engine.render(document);
return (await (this.engine.render(document))).html;
}
}
@@ -14,8 +14,9 @@ import { isMarkdownFile } from '../util/file';
import { normalizeResource, WebviewResourceProvider } from '../util/resources';
import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContentProvider } from './previewContentProvider';
import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider';
import { MarkdownEngine } from '../markdownEngine';
import { urlToUri } from '../util/url';
const localize = nls.loadMessageBundle();
@@ -118,6 +119,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private _disposed: boolean = false;
private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = [];
private readonly _fileWatchersBySrc = new Map</* src: */ string, vscode.FileSystemWatcher>();
constructor(
webview: vscode.WebviewPanel,
resource: vscode.Uri,
@@ -208,6 +211,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
super.dispose();
this._disposed = true;
clearTimeout(this.throttleTimer);
for (const entry of this._fileWatchersBySrc.values()) {
entry.dispose();
}
}
public get resource(): vscode.Uri {
@@ -224,6 +230,10 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
};
}
/**
* The first call immediately refreshes the preview,
* calls happening shortly thereafter are debounced.
*/
public refresh() {
// Schedule update if none is pending
if (!this.throttleTimer) {
@@ -360,7 +370,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource);
}
private setContent(html: string): void {
private setContent(content: MarkdownContentProviderOutput): void {
if (this._disposed) {
return;
}
@@ -371,7 +381,30 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
this._webviewPanel.iconPath = this.iconPath;
this._webviewPanel.webview.options = this.getWebviewOptions();
this._webviewPanel.webview.html = html;
this._webviewPanel.webview.html = content.html;
const srcs = new Set(content.containingImages.map(img => img.src));
// Delete stale file watchers.
for (const [src, watcher] of [...this._fileWatchersBySrc]) {
if (!srcs.has(src)) {
watcher.dispose();
this._fileWatchersBySrc.delete(src);
}
}
// Create new file watchers.
const root = vscode.Uri.joinPath(this._resource, '../');
for (const src of srcs) {
const uri = urlToUri(src, root);
if (uri && uri.scheme === 'file' && !this._fileWatchersBySrc.has(src)) {
const watcher = vscode.workspace.createFileSystemWatcher(uri.fsPath);
watcher.onDidChange(() => {
this.refresh();
});
this._fileWatchersBySrc.set(src, watcher);
}
}
}
private getWebviewOptions(): vscode.WebviewOptions {
@@ -39,6 +39,12 @@ function escapeAttribute(value: string | vscode.Uri): string {
return value.toString().replace(/"/g, '&quot;');
}
export interface MarkdownContentProviderOutput {
html: string;
containingImages: { src: string }[];
}
export class MarkdownContentProvider {
constructor(
private readonly engine: MarkdownEngine,
@@ -54,7 +60,7 @@ export class MarkdownContentProvider {
previewConfigurations: MarkdownPreviewConfigurationManager,
initialLine: number | undefined = undefined,
state?: any
): Promise<string> {
): Promise<MarkdownContentProviderOutput> {
const sourceUri = markdownDocument.uri;
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);
const initialData = {
@@ -75,7 +81,7 @@ export class MarkdownContentProvider {
const csp = this.getCsp(resourceProvider, sourceUri, nonce);
const body = await this.engine.render(markdownDocument);
return `<!DOCTYPE html>
const html = `<!DOCTYPE html>
<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
@@ -89,11 +95,15 @@ export class MarkdownContentProvider {
<base href="${resourceProvider.asWebviewUri(markdownDocument.uri)}">
</head>
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
${body}
${body.html}
<div class="code-line" data-line="${markdownDocument.lineCount}"></div>
${this.getScripts(resourceProvider, nonce)}
</body>
</html>`;
return {
html,
containingImages: body.containingImages,
};
}
public provideFileNotFoundContent(
@@ -54,6 +54,15 @@ class TokenCache {
}
}
export interface RenderOutput {
html: string;
containingImages: { src: string }[];
}
interface RenderEnv {
containingImages: { src: string }[];
}
export class MarkdownEngine {
private md?: Promise<MarkdownIt>;
@@ -141,7 +150,7 @@ export class MarkdownEngine {
return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {});
}
public async render(input: SkinnyTextDocument | string): Promise<string> {
public async render(input: SkinnyTextDocument | string): Promise<RenderOutput> {
const config = this.getConfig(typeof input === 'string' ? undefined : input.uri);
const engine = await this.getEngine(config);
@@ -149,10 +158,19 @@ export class MarkdownEngine {
? this.tokenizeString(input, engine)
: this.tokenizeDocument(input, config, engine);
return engine.renderer.render(tokens, {
const env: RenderEnv = {
containingImages: []
};
const html = engine.renderer.render(tokens, {
...(engine as any).options,
...config
}, {});
}, env);
return {
html,
containingImages: env.containingImages
};
}
public async parse(document: SkinnyTextDocument): Promise<Token[]> {
@@ -192,12 +210,13 @@ export class MarkdownEngine {
private addImageStabilizer(md: any): void {
const original = md.renderer.rules.image;
md.renderer.rules.image = (tokens: any, idx: number, options: any, env: any, self: any) => {
md.renderer.rules.image = (tokens: any, idx: number, options: any, env: RenderEnv, self: any) => {
const token = tokens[idx];
token.attrJoin('class', 'loading');
const src = token.attrGet('src');
if (src) {
env.containingImages.push({ src });
const imgHash = hash(src);
token.attrSet('id', `image-hash-${imgHash}`);
}
@@ -231,6 +250,13 @@ export class MarkdownEngine {
return normalizeLink(vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }).toString());
}
// Support file:// links
if (isOfScheme(Schemes.file, link)) {
// Ensure link is relative by prepending `/` so that it uses the <base> element URI
// when resolving the absolute URL
return normalizeLink('/' + link.replace(/^file:/, 'file'));
}
// If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace
if (!/^[a-z\-]+:/i.test(link)) {
// Use a fake scheme for parsing
@@ -241,12 +267,14 @@ export class MarkdownEngine {
if (uri.path[0] === '/') {
const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!);
if (root) {
const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath);
uri = fileUri.with({
scheme: uri.scheme,
const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({
fragment: uri.fragment,
query: uri.query,
});
// Ensure fileUri is relative by prepending `/` so that it uses the <base> element URI
// when resolving the absolute URL
uri = vscode.Uri.parse('markdown-link:' + '/' + fileUri.toString(true).replace(/^\S+?:/, fileUri.scheme));
}
}
@@ -269,9 +297,7 @@ export class MarkdownEngine {
private addLinkValidator(md: any): void {
const validateLink = md.validateLink;
md.validateLink = (link: string) => {
// support file:// links
return validateLink(link)
|| isOfScheme(Schemes.file, link)
|| isOfScheme(Schemes.vscode, link)
|| isOfScheme(Schemes['vscode-insiders'], link)
|| /^data:image\/.*?;/.test(link);
@@ -21,12 +21,31 @@ suite('markdown.engine', () => {
test('Renders a document', async () => {
const doc = new InMemoryDocument(testFileName, input);
const engine = createNewMarkdownEngine();
assert.strictEqual(await engine.render(doc), output);
assert.strictEqual((await engine.render(doc)).html, output);
});
test('Renders a string', async () => {
const engine = createNewMarkdownEngine();
assert.strictEqual(await engine.render(input), output);
assert.strictEqual((await engine.render(input)).html, output);
});
});
suite('image-caching', () => {
const input = '![](img.png) [](no-img.png) ![](http://example.org/img.png) ![](img.png) ![](./img2.png)';
test('Extracts all images', async () => {
const engine = createNewMarkdownEngine();
assert.deepStrictEqual((await engine.render(input)), {
html: '<p data-line="0" class="code-line">'
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435"> '
+ '<a href="no-img.png" data-href="no-img.png"></a> '
+ '<img src="http://example.org/img.png" alt="" class="loading" id="image-hash--1903814170"> '
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435"> '
+ '<img src="./img2.png" alt="" class="loading" id="image-hash-265238964">'
+ '</p>\n'
,
containingImages: [{ src: 'img.png' }, { src: 'http://example.org/img.png' }, { src: 'img.png' }, { src: './img2.png' }],
});
});
});
});
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual } from 'assert';
import 'mocha';
import { Uri } from 'vscode';
import { urlToUri } from '../util/url';
suite('urlToUri', () => {
test('Absolute File', () => {
deepStrictEqual(
urlToUri('file:///root/test.txt', Uri.parse('file:///usr/home/')),
Uri.parse('file:///root/test.txt')
);
});
test('Relative File', () => {
deepStrictEqual(
urlToUri('./file.ext', Uri.parse('file:///usr/home/')),
Uri.parse('file:///usr/home/file.ext')
);
});
test('Http Basic', () => {
deepStrictEqual(
urlToUri('http://example.org?q=10&f', Uri.parse('file:///usr/home/')),
Uri.parse('http://example.org?q=10&f')
);
});
test('Http Encoded Chars', () => {
deepStrictEqual(
urlToUri('http://example.org/%C3%A4', Uri.parse('file:///usr/home/')),
Uri.parse('http://example.org/%C3%A4')
);
});
});
@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
declare const URL: typeof import('url').URL;
/**
* Tries to convert an url into a vscode uri and returns undefined if this is not possible.
* `url` can be absolute or relative.
*/
export function urlToUri(url: string, base: vscode.Uri): vscode.Uri | undefined {
try {
// `vscode.Uri.joinPath` cannot be used, since it understands
// `src` as path, not as relative url. This is problematic for query args.
const parsedUrl = new URL(url, base.toString());
const uri = vscode.Uri.parse(parsedUrl.toString());
return uri;
} catch (e) {
// Don't crash if `URL` cannot parse `src`.
return undefined;
}
}
@@ -48,7 +48,7 @@ export class Keychain {
async setToken(token: string): Promise<void> {
try {
return await this.context.secrets.set(SERVICE_ID, token);
return await this.context.secrets.store(SERVICE_ID, token);
} catch (e) {
Logger.error(`Setting token failed: ${e}`);
+10 -1
View File
@@ -223,9 +223,18 @@ function parseSearchResults(document: vscode.TextDocument, token?: vscode.Cancel
const metadataOffset = (indentation + _lineNumber + seperator).length;
const targetRange = new vscode.Range(Math.max(lineNumber - 3, 0), 0, lineNumber + 3, line.length);
let locations: Required<vscode.LocationLink>[] = [];
// Allow line number, indentation, etc to take you to definition as well.
locations.push({
targetRange,
targetSelectionRange: new vscode.Range(lineNumber, 0, lineNumber, 1),
targetUri: currentTarget,
originSelectionRange: new vscode.Range(i, 0, i, resultStart),
});
let lastEnd = resultStart;
let offset = 0;
let locations: Required<vscode.LocationLink>[] = [];
ELISION_REGEX.lastIndex = resultStart;
for (let match: RegExpExecArray | null; (match = ELISION_REGEX.exec(line));) {
locations.push({
@@ -31,7 +31,7 @@ const jsTsLanguageConfiguration: vscode.LanguageConfiguration = {
}, {
// e.g. * ...|
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
oneLineAboveText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/,
previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/,
action: { indentAction: vscode.IndentAction.None, appendText: '* ' },
}, {
// e.g. */|
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.53.0",
"distro": "dd764d38c8e0aa3914484c1e62300f51cdd7c5b8",
"distro": "826dce393851992f3b4af928e2bf7dd3813843cc",
"author": {
"name": "Microsoft Corporation"
},
@@ -38,7 +38,7 @@
"smoketest-no-compile": "cd test/smoke && node test/index.js",
"download-builtin-extensions": "node build/lib/builtInExtensions.js",
"monaco-compile-check": "tsc -p src/tsconfig.monaco.json --noEmit",
"tsec-compile-check": "node node_modules/vscode-tsec/bin/tsec -p src/tsconfig.tsec.json",
"tsec-compile-check": "node node_modules/tsec/bin/tsec -p src/tsconfig.tsec.json",
"valid-layers-check": "node build/lib/layersChecker.js",
"strict-function-types-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictFunctionTypes",
"update-distro": "node build/npm/update-distro.js",
@@ -189,6 +189,7 @@
"source-map": "0.6.1",
"source-map-support": "^0.3.2",
"style-loader": "^1.0.0",
"tsec": "0.1.1",
"ts-loader": "^6.2.1",
"typescript": "4.2.0-dev.20201207",
"typescript-formatter": "7.1.0",
@@ -198,7 +199,6 @@
"vscode-debugprotocol": "1.43.0",
"vscode-nls-dev": "^3.3.1",
"vscode-telemetry-extractor": "^1.6.0",
"vscode-tsec": "0.0.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"webpack-stream": "^5.2.1",
Binary file not shown.
+4 -2
View File
@@ -126,9 +126,11 @@ export class MenuBar extends Disposable {
let eventHandled = true;
const key = !!e.key ? e.key.toLocaleLowerCase() : '';
if (event.equals(KeyCode.LeftArrow) || (isMacintosh && event.equals(KeyCode.Tab | KeyMod.Shift))) {
const tabNav = isMacintosh && this.options.compactMode === undefined;
if (event.equals(KeyCode.LeftArrow) || (tabNav && event.equals(KeyCode.Tab | KeyMod.Shift))) {
this.focusPrevious();
} else if (event.equals(KeyCode.RightArrow) || (isMacintosh && event.equals(KeyCode.Tab))) {
} else if (event.equals(KeyCode.RightArrow) || (tabNav && event.equals(KeyCode.Tab))) {
this.focusNext();
} else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) {
this.setUnfocusedState();
@@ -152,7 +152,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
const diffIdentityProvider = options.diffIdentityProvider && wrapIdentityProvider(options.diffIdentityProvider);
if (element === null) {
const compressedChildren = Iterable.map(children, this.enabled ? compress : noCompress);
this._setChildren(null, compressedChildren, { diffIdentityProvider, diffDeep: Infinity });
this._setChildren(null, compressedChildren, { diffIdentityProvider, diffDepth: Infinity });
return;
}
@@ -175,7 +175,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
this._setChildren(parent.element, parentChildren, {
diffIdentityProvider,
diffDeep: node.depth - parent.depth,
diffDepth: node.depth - parent.depth,
});
}
@@ -199,7 +199,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
// provider is available, since we know the raw nodes are unchanged.
this._setChildren(null, recompressedRootChildren, {
diffIdentityProvider: this.identityProvider,
diffDeep: Infinity,
diffDepth: Infinity,
});
}
@@ -48,7 +48,7 @@ export interface IIndexedSpliceOptions<T, TFilterData> {
* If set, child updates will recurse the given number of levels even if
* items in the splice operation are unchanged. `Infinity` is a valid value.
*/
readonly diffDeep?: number;
readonly diffDepth?: number;
/**
* Identity provider used to optimize splice() calls in the IndexTree. If
@@ -159,7 +159,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
deleteCount: number,
toInsertIterable: Iterable<ITreeElement<T>> = Iterable.empty(),
options: IIndexedSpliceOptions<T, TFilterData>,
recurseLevels = options.diffDeep ?? 0,
recurseLevels = options.diffDepth ?? 0,
) {
const { parentNode } = this.getParentNodeWithListIndex(location);
const toInsert = [...toInsertIterable];
+2
View File
@@ -531,6 +531,8 @@ export namespace Codicon {
export const passFilled = new Codicon('pass-filled', { character: '\\ebb3' });
export const circleLargeFilled = new Codicon('circle-large-filled', { character: '\\ebb4' });
export const circleLargeOutline = new Codicon('circle-large-outline', { character: '\\ebb5' });
export const combine = new Codicon('combine', { character: '\\ebb6' });
export const gather = new Codicon('gather', { character: '\\ebb6' });
export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition);
}
@@ -37,6 +37,11 @@ export class Client extends MessagePortClient implements IDisposable {
*/
export async function connect(window: BrowserWindow): Promise<MessagePortMain> {
// Assert healthy window to talk to
if (window.webContents.isDestroyed()) {
throw new Error('ipc.mp#connect: Cannot talk to window because it is closed or destroyed');
}
// Ask to create message channel inside the window
// and send over a UUID to correlate the response
const nonce = generateUuid();
@@ -242,7 +242,7 @@ suite('IndexTreeModel', () => {
{ element: 0, children: [{ element: 0.1 }] },
{ element: 1 },
{ element: 2, children: [{ element: 2.1 }, { element: 2.2, children: [{ element: 2.21 }] }] },
], { diffIdentityProvider, diffDeep: Infinity });
], { diffIdentityProvider, diffDepth: Infinity });
assert.deepStrictEqual(list.filter(l => l.depth === 1).map(toElements), [
-0.5,
+23 -5
View File
@@ -270,8 +270,13 @@ export class CodeApplication extends Disposable {
//#region Bootstrap IPC Handlers
let slowShellResolveWarningShown = false;
ipcMain.on('vscode:fetchShellEnv', async event => {
const window = this.windowsMainService?.getWindowByWebContents(event.sender);
// DO NOT remove: not only usual windows are fetching the
// shell environment but also shared process, issue reporter
// etc, so we need to reply via `webContents` always
const webContents = event.sender;
let replied = false;
@@ -282,15 +287,25 @@ export class CodeApplication extends Disposable {
if (!replied) {
replied = true;
window?.send('vscode:acceptShellEnv', env);
if (!webContents.isDestroyed()) {
webContents.send('vscode:acceptShellEnv', env);
}
}
}
// Handle slow shell environment resolve calls:
// - a warning after 3s but continue to resolve
// - an error after 10s and stop trying to resolve
// - a warning after 3s but continue to resolve (only once in active window)
// - an error after 10s and stop trying to resolve (in every window where this happens)
const cts = new CancellationTokenSource();
const shellEnvSlowWarningHandle = setTimeout(() => window?.sendWhenReady('vscode:showShellEnvSlowWarning', cts.token), 3000);
const shellEnvSlowWarningHandle = setTimeout(() => {
if (!slowShellResolveWarningShown) {
this.windowsMainService?.sendToFocused('vscode:showShellEnvSlowWarning', cts.token);
slowShellResolveWarningShown = true;
}
}, 3000);
const window = this.windowsMainService?.getWindowByWebContents(event.sender); // Note: this can be `undefined` for the shared process!!
const shellEnvTimeoutErrorHandle = setTimeout(() => {
cts.dispose(true);
window?.sendWhenReady('vscode:showShellEnvTimeoutError', CancellationToken.None);
@@ -302,6 +317,9 @@ export class CodeApplication extends Disposable {
// a first window was opened from the UI but a second
// from the CLI and that has implications for whether to
// resolve the shell environment or not.
//
// Window can be undefined for e.g. the shared process
// that is not part of our windows registry!
let args: NativeParsedArgs;
let env: NodeJS.ProcessEnv;
if (window?.config) {
+13 -6
View File
@@ -14,6 +14,7 @@ import { browserCodeLoadingCacheStrategy } from 'vs/base/common/platform';
import { ISharedProcess, ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess';
import { Disposable } from 'vs/base/common/lifecycle';
import { connect as connectMessagePort } from 'vs/base/parts/ipc/electron-main/ipc.mp';
import { assertIsDefined } from 'vs/base/common/types';
export class SharedProcess extends Disposable implements ISharedProcess {
@@ -49,8 +50,18 @@ export class SharedProcess extends Disposable implements ISharedProcess {
// workbench window will communicate directly
await this.whenReady();
// connect to the shared process window
const port = await this.connect();
// Check back if the requesting window meanwhile closed
// Since shared process is delayed on startup there is
// a chance that the window close before the shared process
// was ready for a connection.
if (e.sender.isDestroyed()) {
return port.close();
}
// send the port back to the requesting window
e.sender.postMessage('vscode:createSharedProcessMessageChannelResult', nonce, [port]);
});
}
@@ -208,13 +219,9 @@ export class SharedProcess extends Disposable implements ISharedProcess {
// Wait for shared process being ready to accept connection
await this.whenIpcReady;
// Assert healthy shared process window
if (!this.window || this.window.webContents.isDestroyed()) {
throw new Error('Cannot connect to shared process window because the window is closed or destroyed');
}
// Connect and return message port
return connectMessagePort(this.window);
const window = assertIsDefined(this.window);
return connectMessagePort(window);
}
async toggle(): Promise<void> {
+22 -9
View File
@@ -424,11 +424,12 @@ export class CodeWindow extends Disposable implements ICodeWindow {
this.dispose();
});
// Prevent loading of svgs
this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => {
if (details.url.indexOf('.svg') > 0) {
const svgFileSchemes = new Set([Schemas.file, Schemas.vscodeFileResource]);
this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => {
// Prevent loading of remote svgs
if (details.url.endsWith('.svg')) {
const uri = URI.parse(details.url);
if (uri && !uri.scheme.match(/file/i) && uri.path.endsWith('.svg')) {
if (uri && !svgFileSchemes.has(uri.scheme)) {
return callback({ cancel: true });
}
}
@@ -436,12 +437,24 @@ export class CodeWindow extends Disposable implements ICodeWindow {
return callback({});
});
this._win.webContents.session.webRequest.onHeadersReceived(null!, (details, callback) => {
this._win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
const responseHeaders = details.responseHeaders as Record<string, (string) | (string[])>;
const contentType = (responseHeaders['content-type'] || responseHeaders['Content-Type']);
if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) {
return callback({ cancel: true });
if (contentType && Array.isArray(contentType)) {
// https://github.com/microsoft/vscode/issues/97564
// ensure local svg files have Content-Type image/svg+xml
if (details.url.endsWith('.svg')) {
const uri = URI.parse(details.url);
if (uri && svgFileSchemes.has(uri.scheme)) {
responseHeaders['Content-Type'] = ['image/svg+xml'];
return callback({ cancel: false, responseHeaders });
}
}
if (contentType.some(x => x.toLowerCase().includes('image/svg'))) {
return callback({ cancel: true });
}
}
return callback({ cancel: false });
@@ -1254,7 +1267,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
send(channel: string, ...args: any[]): void {
if (this._win) {
if (this._win.isDestroyed()) {
if (this._win.webContents.isDestroyed()) {
this.logService.warn(`Sending IPC message to channel ${channel} for window that is destroyed`);
return;
}
@@ -3,7 +3,8 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' vscode-webview: https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' vscode-webview: https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https: ws:; font-src 'self' https: vscode-remote-resource:;">
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types default TrustedFunctionWorkaround ExtensionScripts amdLoader cellRendererEditorText defaultWorkerFactory diffEditorWidget domLineBreaksComputer editorViewLayer extensionHostWorker insane notebookOutputRenderer safeInnerHtml standaloneColorizer tokenizeToString webNestedWorkerExtensionHost webWorkerExtensionHost;">
</head>
<body aria-label="">
</body>
@@ -56,9 +56,9 @@
if (value === '<!DOCTYPE html><style type="text/css">:host { display: flex; }</style>') {
return value;
}
// throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached');
console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached');
return value;
throw new Error('UNTRUSTED html usage, default trusted types policy should NEVER be reached');
// console.trace('UNTRUSTED html usage, default trusted types policy should NEVER be reached');
// return value;
}
});
+14 -14
View File
@@ -3,15 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as fs from 'fs';
import { homedir } from 'os';
import { existsSync, statSync, unlinkSync, chmodSync, truncateSync, readFileSync } from 'fs';
import { spawn, ChildProcess, SpawnOptions } from 'child_process';
import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { parseCLIProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper';
import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile';
import product from 'vs/platform/product/common/product';
import * as paths from 'vs/base/common/path';
import { isAbsolute, join } from 'vs/base/common/path';
import { whenDeleted, writeFileSync } from 'vs/base/node/pfs';
import { findFreePort, randomPort } from 'vs/base/node/ports';
import { isWindows, isLinux } from 'vs/base/common/platform';
@@ -69,10 +69,10 @@ export async function main(argv: string[]): Promise<any> {
// Validate
if (
!source || !target || source === target || // make sure source and target are provided and are not the same
!paths.isAbsolute(source) || !paths.isAbsolute(target) || // make sure both source and target are absolute paths
!fs.existsSync(source) || !fs.statSync(source).isFile() || // make sure source exists as file
!fs.existsSync(target) || !fs.statSync(target).isFile() // make sure target exists as file
!source || !target || source === target || // make sure source and target are provided and are not the same
!isAbsolute(source) || !isAbsolute(target) || // make sure both source and target are absolute paths
!existsSync(source) || !statSync(source).isFile() || // make sure source exists as file
!existsSync(target) || !statSync(target).isFile() // make sure target exists as file
) {
throw new Error('Using --file-write with invalid arguments.');
}
@@ -83,15 +83,15 @@ export async function main(argv: string[]): Promise<any> {
let targetMode: number = 0;
let restoreMode = false;
if (!!args['file-chmod']) {
targetMode = fs.statSync(target).mode;
targetMode = statSync(target).mode;
if (!(targetMode & 128) /* readonly */) {
fs.chmodSync(target, targetMode | 128);
chmodSync(target, targetMode | 128);
restoreMode = true;
}
}
// Write source to target
const data = fs.readFileSync(source);
const data = readFileSync(source);
if (isWindows) {
// On Windows we use a different strategy of saving the file
// by first truncating the file and then writing with r+ mode.
@@ -99,7 +99,7 @@ export async function main(argv: string[]): Promise<any> {
// (see https://github.com/microsoft/vscode/issues/931) and
// prevent removing alternate data streams
// (see https://github.com/microsoft/vscode/issues/6363)
fs.truncateSync(target, 0);
truncateSync(target, 0);
writeFileSync(target, data, { flag: 'r+' });
} else {
writeFileSync(target, data);
@@ -107,7 +107,7 @@ export async function main(argv: string[]): Promise<any> {
// Restore previous mode as needed
if (restoreMode) {
fs.chmodSync(target, targetMode);
chmodSync(target, targetMode);
}
} catch (error) {
error.message = `Error using --file-write: ${error.message}`;
@@ -215,7 +215,7 @@ export async function main(argv: string[]): Promise<any> {
throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.');
}
const filenamePrefix = paths.join(os.homedir(), 'prof-' + Math.random().toString(16).slice(-4));
const filenamePrefix = join(homedir(), 'prof-' + Math.random().toString(16).slice(-4));
addArg(argv, `--inspect-brk=${portMain}`);
addArg(argv, `--remote-debugging-port=${portRenderer}`);
@@ -338,7 +338,7 @@ export async function main(argv: string[]): Promise<any> {
// Make sure to delete the tmp stdin file if we have any
if (stdinFilePath) {
fs.unlinkSync(stdinFilePath);
unlinkSync(stdinFilePath);
}
});
}
+149 -83
View File
@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { release } from 'os';
import * as fs from 'fs';
import { gracefulify } from 'graceful-fs';
import { isAbsolute, join } from 'vs/base/common/path';
import { raceTimeout } from 'vs/base/common/async';
import product from 'vs/platform/product/common/product';
@@ -36,108 +38,119 @@ import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry';
import { FileService } from 'vs/platform/files/common/fileService';
import { IFileService } from 'vs/platform/files/common/files';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IProductService } from 'vs/platform/product/common/productService';
import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService';
import { URI } from 'vs/base/common/uri';
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { toErrorMessage } from 'vs/base/common/errorMessage';
export class Main {
export class CliMain extends Disposable {
constructor(
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IExtensionManagementCLIService private readonly extensionManagementCLIService: IExtensionManagementCLIService
) { }
private argv: NativeParsedArgs
) {
super();
async run(argv: NativeParsedArgs): Promise<void> {
if (argv['install-source']) {
await this.setInstallSource(argv['install-source']);
return;
// Enable gracefulFs
gracefulify(fs);
this.registerListeners();
}
private registerListeners(): void {
// Dispose on exit
process.once('exit', () => this.dispose());
}
async run(): Promise<void> {
// Services
const [instantiationService, appenders] = await this.initServices();
return instantiationService.invokeFunction(async accessor => {
const logService = accessor.get(ILogService);
const environmentService = accessor.get(INativeEnvironmentService);
const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService);
// Log info
logService.info('CLI main', this.argv);
// Error handler
this.registerErrorHandler(logService);
// Run based on argv
await this.doRun(environmentService, extensionManagementCLIService);
// Flush the remaining data in AI adapter (with 1s timeout)
return raceTimeout(combinedAppender(...appenders).flush(), 1000);
});
}
private async initServices(): Promise<[IInstantiationService, AppInsightsAppender[]]> {
const services = new ServiceCollection();
// Environment
const environmentService = new NativeEnvironmentService(this.argv);
services.set(IEnvironmentService, environmentService);
services.set(INativeEnvironmentService, environmentService);
// Init folders
await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? mkdirp(path) : undefined));
// Log
const logLevel = getLogLevel(environmentService);
const loggers: ILogService[] = [];
loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel));
if (logLevel === LogLevel.Trace) {
loggers.push(new ConsoleLogService(logLevel));
}
if (argv['list-extensions']) {
await this.extensionManagementCLIService.listExtensions(!!argv['show-versions'], argv['category']);
} else if (argv['install-extension'] || argv['install-builtin-extension']) {
await this.extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(argv['install-extension'] || []), argv['install-builtin-extension'] || [], !!argv['do-not-sync'], !!argv['force']);
} else if (argv['uninstall-extension']) {
await this.extensionManagementCLIService.uninstallExtensions(this.asExtensionIdOrVSIX(argv['uninstall-extension']), !!argv['force']);
} else if (argv['locate-extension']) {
await this.extensionManagementCLIService.locateExtension(argv['locate-extension']);
} else if (argv['telemetry']) {
console.log(buildTelemetryMessage(this.environmentService.appRoot, this.environmentService.extensionsPath));
}
}
const logService = this._register(new MultiplexLogService(loggers));
services.set(ILogService, logService);
private asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] {
return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(process.cwd(), input)) : input);
}
// Files
const fileService = this._register(new FileService(logService));
services.set(IFileService, fileService);
private setInstallSource(installSource: string): Promise<void> {
return writeFile(this.environmentService.installSourcePath, installSource.slice(0, 30));
}
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
}
// Configuration
const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService));
services.set(IConfigurationService, configurationService);
const eventPrefix = 'monacoworkbench';
// Init config
await configurationService.initialize();
export async function main(argv: NativeParsedArgs): Promise<void> {
const services = new ServiceCollection();
const disposables = new DisposableStore();
// State
const stateService = new StateService(environmentService, logService);
services.set(IStateService, stateService);
const environmentService = new NativeEnvironmentService(argv);
const logLevel = getLogLevel(environmentService);
const loggers: ILogService[] = [];
loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel));
if (logLevel === LogLevel.Trace) {
loggers.push(new ConsoleLogService(logLevel));
}
const logService = new MultiplexLogService(loggers);
process.once('exit', () => logService.dispose());
logService.info('main', argv);
await Promise.all<void | undefined>([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath]
.map((path): undefined | Promise<void> => path ? mkdirp(path) : undefined));
// Files
const fileService = new FileService(logService);
disposables.add(fileService);
services.set(IFileService, fileService);
const diskFileSystemProvider = new DiskFileSystemProvider(logService);
disposables.add(diskFileSystemProvider);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
const configurationService = new ConfigurationService(environmentService.settingsResource, fileService);
disposables.add(configurationService);
await configurationService.initialize();
services.set(IEnvironmentService, environmentService);
services.set(INativeEnvironmentService, environmentService);
services.set(ILogService, logService);
services.set(IConfigurationService, configurationService);
services.set(IStateService, new SyncDescriptor(StateService));
services.set(IProductService, { _serviceBrand: undefined, ...product });
const instantiationService: IInstantiationService = new InstantiationService(services);
return instantiationService.invokeFunction(async accessor => {
const stateService = accessor.get(IStateService);
// Product
services.set(IProductService, { _serviceBrand: undefined, ...product });
const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService;
const services = new ServiceCollection();
// Request
services.set(IRequestService, new SyncDescriptor(RequestService));
// Extensions
services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService));
// Localizations
services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService));
// Telemetry
const appenders: AppInsightsAppender[] = [];
if (isBuilt && !extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) {
if (product.aiConfig && product.aiConfig.asimovKey) {
appenders.push(new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey));
appenders.push(new AppInsightsAppender('monacoworkbench', null, product.aiConfig.asimovKey));
}
const config: ITelemetryServiceConfig = {
@@ -153,17 +166,70 @@ export async function main(argv: NativeParsedArgs): Promise<void> {
services.set(ITelemetryService, NullTelemetryService);
}
const instantiationService2 = instantiationService.createChild(services);
const main = instantiationService2.createInstance(Main);
return [new InstantiationService(services), appenders];
}
try {
await main.run(argv);
private registerErrorHandler(logService: ILogService): void {
// Flush the remaining data in AI adapter.
// If it does not complete in 1 second, exit the process.
await raceTimeout(combinedAppender(...appenders).flush(), 1000);
} finally {
disposables.dispose();
// Install handler for unexpected errors
setUnexpectedErrorHandler(error => {
const message = toErrorMessage(error, true);
if (!message) {
return;
}
logService.error(message);
});
}
private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService): Promise<void> {
// Install Source
if (this.argv['install-source']) {
return this.setInstallSource(environmentService, this.argv['install-source']);
}
});
// List Extensions
if (this.argv['list-extensions']) {
return extensionManagementCLIService.listExtensions(!!this.argv['show-versions'], this.argv['category']);
}
// 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']);
}
// Uninstall Extension
else if (this.argv['uninstall-extension']) {
return extensionManagementCLIService.uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force']);
}
// Locate Extension
else if (this.argv['locate-extension']) {
return extensionManagementCLIService.locateExtension(this.argv['locate-extension']);
}
// Telemetry
else if (this.argv['telemetry']) {
console.log(buildTelemetryMessage(environmentService.appRoot, environmentService.extensionsPath));
}
}
private asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] {
return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(process.cwd(), input)) : input);
}
private setInstallSource(environmentService: INativeEnvironmentService, installSource: string): Promise<void> {
return writeFile(environmentService.installSourcePath, installSource.slice(0, 30));
}
}
export async function main(argv: NativeParsedArgs): Promise<void> {
const cliMain = new CliMain(argv);
try {
await cliMain.run();
} finally {
cliMain.dispose();
}
}
@@ -103,7 +103,7 @@ export class MarkdownRenderer {
},
asyncRenderCallback: () => this._onDidRenderAsync.fire(),
actionHandler: {
callback: (content) => this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError),
callback: (content) => this._openerService.open(content, { fromUserGesture: true, allowContributedOpeners: true }).catch(onUnexpectedError),
disposeables
}
};
@@ -211,10 +211,16 @@ export class OpenerService implements IOpenerService {
href = encodeURI(resolved.toString(true));
}
for (const opener of this._externalOpeners) {
const didOpen = await opener.openExternal(href, { sourceUri: uri }, CancellationToken.None);
if (didOpen) {
return true;
if (options?.allowContributedOpeners) {
const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined;
for (const opener of this._externalOpeners) {
const didOpen = await opener.openExternal(href, {
sourceUri: uri,
preferredOpenerId,
}, CancellationToken.None);
if (didOpen) {
return true;
}
}
}
@@ -150,7 +150,7 @@ export interface OnEnterRule {
/**
* This rule will only execute if the text above the this line matches this regular expression.
*/
oneLineAboveText?: RegExp;
previousLineText?: RegExp;
/**
* The action to execute.
*/
@@ -101,11 +101,11 @@ export class RichEditSupport {
return this._electricCharacter;
}
public onEnter(autoIndent: EditorAutoIndentStrategy, oneLineAboveText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null {
public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null {
if (!this._onEnterSupport) {
return null;
}
return this._onEnterSupport.onEnter(autoIndent, oneLineAboveText, beforeEnterText, afterEnterText);
return this._onEnterSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText);
}
private static _mergeConf(prev: LanguageConfiguration | null, current: LanguageConfiguration): LanguageConfiguration {
@@ -700,17 +700,17 @@ export class LanguageConfigurationRegistryImpl {
afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset);
}
let oneLineAboveText = '';
let previousLineText = '';
if (range.startLineNumber > 1 && scopedLineTokens.firstCharOffset === 0) {
// This is not the first line and the entire line belongs to this mode
const oneLineAboveScopedLineTokens = this.getScopedLineTokens(model, range.startLineNumber - 1);
if (oneLineAboveScopedLineTokens.languageId === scopedLineTokens.languageId) {
// The line above ends with text belonging to the same mode
oneLineAboveText = oneLineAboveScopedLineTokens.getLineContent();
previousLineText = oneLineAboveScopedLineTokens.getLineContent();
}
}
const enterResult = richEditSupport.onEnter(autoIndent, oneLineAboveText, beforeEnterText, afterEnterText);
const enterResult = richEditSupport.onEnter(autoIndent, previousLineText, beforeEnterText, afterEnterText);
if (!enterResult) {
return null;
}
@@ -49,7 +49,7 @@ export class OnEnterSupport {
this._regExpRules = opts.onEnterRules || [];
}
public onEnter(autoIndent: EditorAutoIndentStrategy, oneLineAboveText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null {
public onEnter(autoIndent: EditorAutoIndentStrategy, previousLineText: string, beforeEnterText: string, afterEnterText: string): EnterAction | null {
// (1): `regExpRules`
if (autoIndent >= EditorAutoIndentStrategy.Advanced) {
for (let i = 0, len = this._regExpRules.length; i < len; i++) {
@@ -61,8 +61,8 @@ export class OnEnterSupport {
reg: rule.afterText,
text: afterEnterText
}, {
reg: rule.oneLineAboveText,
text: oneLineAboveText
reg: rule.previousLineText,
text: previousLineText
}].every((obj): boolean => {
return obj.reg ? obj.reg.test(obj.text) : true;
});
@@ -299,13 +299,13 @@ export class MoveLinesCommand implements ICommand {
}
}
private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, oneLineAboveText?: string) {
private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, previousLineText?: string) {
let validPrecedingLine = oneLineAbove;
while (validPrecedingLine >= 1) {
// ship empty lines as empty lines just inherit indentation
let lineContent;
if (validPrecedingLine === oneLineAbove && oneLineAboveText !== undefined) {
lineContent = oneLineAboveText;
if (validPrecedingLine === oneLineAbove && previousLineText !== undefined) {
lineContent = previousLineText;
} else {
lineContent = model.getLineContent(validPrecedingLine);
}
+1 -1
View File
@@ -327,7 +327,7 @@ export class LinkDetector implements IEditorContribution {
}
}
return this.openerService.open(uri, { openToSide, fromUserGesture });
return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true });
}, err => {
const messageOrError =
@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { first } from 'vs/base/common/async';
import { onUnexpectedExternalError } from 'vs/base/common/errors';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { ITextModel } from 'vs/editor/common/model';
@@ -20,19 +19,26 @@ export const Context = {
MultipleSignatures: new RawContextKey<boolean>('parameterHintsMultipleSignatures', false),
};
export function provideSignatureHelp(
export async function provideSignatureHelp(
model: ITextModel,
position: Position,
context: modes.SignatureHelpContext,
token: CancellationToken
): Promise<modes.SignatureHelpResult | null | undefined> {
): Promise<modes.SignatureHelpResult | undefined> {
const supports = modes.SignatureHelpProviderRegistry.ordered(model);
return first(supports.map(support => () => {
return Promise.resolve(support.provideSignatureHelp(model, position, token, context))
.catch<modes.SignatureHelpResult | undefined>(e => onUnexpectedExternalError(e));
}));
for (const support of supports) {
try {
const result = await support.provideSignatureHelp(model, position, token, context);
if (result) {
return result;
}
} catch (err) {
onUnexpectedExternalError(err);
}
}
return undefined;
}
CommandsRegistry.registerCommand('_executeSignatureHelpProvider', async (accessor, ...args: [URI, IPosition, string?]) => {
@@ -18,7 +18,7 @@ export const javascriptOnEnterRules = [
}, {
// e.g. * ...|
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
oneLineAboveText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/,
previousLineText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/,
action: { indentAction: IndentAction.None, appendText: '* ' }
}, {
// e.g. */|
@@ -51,8 +51,8 @@ suite('OnEnter', () => {
let support = new OnEnterSupport({
onEnterRules: javascriptOnEnterRules
});
let testIndentAction = (oneLineAboveText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => {
let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, oneLineAboveText, beforeText, afterText);
let testIndentAction = (previousLineText: string, beforeText: string, afterText: string, expectedIndentAction: IndentAction | null, expectedAppendText: string | null, removeText: number = 0) => {
let actual = support.onEnter(EditorAutoIndentStrategy.Advanced, previousLineText, beforeText, afterText);
if (expectedIndentAction === null) {
assert.strictEqual(actual, null, 'isNull:' + beforeText);
} else {
+1 -1
View File
@@ -5417,7 +5417,7 @@ declare namespace monaco.languages {
/**
* This rule will only execute if the text above the this line matches this regular expression.
*/
oneLineAboveText?: RegExp;
previousLineText?: RegExp;
/**
* The action to execute.
*/
@@ -42,12 +42,16 @@ export class ExtensionIdentifierWithVersion implements IExtensionIdentifierWithV
}
}
export function getExtensionId(publisher: string, name: string): string {
return `${publisher}.${name}`;
}
export function adoptToGalleryExtensionId(id: string): string {
return id.toLocaleLowerCase();
}
export function getGalleryExtensionId(publisher: string, name: string): string {
return `${publisher.toLocaleLowerCase()}.${name.toLocaleLowerCase()}`;
return adoptToGalleryExtensionId(getExtensionId(publisher, name));
}
export function groupByExtension<T>(extensions: T[], getExtensionIdentifier: (t: T) => IExtensionIdentifier): T[][] {
+6 -2
View File
@@ -32,7 +32,11 @@ export type OpenInternalOptions = {
readonly fromUserGesture?: boolean;
};
export type OpenExternalOptions = { readonly openExternal?: boolean; readonly allowTunneling?: boolean };
export type OpenExternalOptions = {
readonly openExternal?: boolean;
readonly allowTunneling?: boolean;
readonly allowContributedOpeners?: boolean | string;
};
export type OpenOptions = OpenInternalOptions & OpenExternalOptions;
@@ -47,7 +51,7 @@ export interface IOpener {
}
export interface IExternalOpener {
openExternal(href: string, ctx: { sourceUri: URI }, token: CancellationToken): Promise<boolean>;
openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise<boolean>;
dispose?(): void;
}
+26
View File
@@ -42,6 +42,24 @@ export interface ITunnelProvider {
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel | undefined> | undefined;
}
export interface ITunnel {
remoteAddress: { port: number, host: string };
/**
* The complete local address(ex. localhost:1234)
*/
localAddress: string;
public?: boolean;
/**
* Implementers of Tunnel should fire onDidDispose when dispose is called.
*/
onDidDispose: Event<void>;
dispose(): Promise<void> | void;
}
export interface ITunnelService {
readonly _serviceBrand: undefined;
@@ -166,6 +184,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
}
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false): Promise<RemoteTunnel | undefined> | undefined {
this.logService.trace(`openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`);
if (!addressProvider) {
return undefined;
}
@@ -176,14 +195,17 @@ export abstract class AbstractTunnelService implements ITunnelService {
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic);
if (!resolvedTunnel) {
this.logService.trace(`Tunnel was not created.`);
return resolvedTunnel;
}
return resolvedTunnel.then(tunnel => {
if (!tunnel) {
this.logService.trace('New tunnel is undefined.');
this.removeEmptyTunnelFromMap(remoteHost!, remotePort);
return undefined;
}
this.logService.trace('New tunnel established.');
const newTunnel = this.makeTunnel(tunnel);
if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) {
this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.');
@@ -215,6 +237,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel | undefined> }): Promise<void> {
if (tunnel.refcount <= 0) {
this.logService.trace(`Tunnel is being disposed ${remoteHost}:${remotePort}.`);
const disposePromise: Promise<void> = tunnel.value.then(async (tunnel) => {
if (tunnel) {
await tunnel.dispose(true);
@@ -282,10 +305,13 @@ export abstract class AbstractTunnelService implements ITunnelService {
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined;
protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise<RemoteTunnel | undefined> | undefined {
this.logService.trace(`Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false };
const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic };
const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo);
this.logService.trace('Tunnel created by provider.');
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}
@@ -150,6 +150,7 @@ export class BaseTunnelService extends AbstractTunnelService {
if (this._tunnelProvider) {
return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic);
} else {
this.logService.trace(`Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
const options: IConnectionOptions = {
commit: this.productService.commit,
socketFactory: this.socketFactory,
@@ -160,6 +161,7 @@ export class BaseTunnelService extends AbstractTunnelService {
};
const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
this.logService.trace('Tunnel created without provider.');
this.addTunnelToMap(remoteHost, remotePort, tunnel);
return tunnel;
}
+2
View File
@@ -12,6 +12,8 @@ export interface IStateService {
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue?: T): T | undefined;
setItem(key: string, data?: object | string | number | boolean | undefined | null): void;
removeItem(key: string): void;
}
+1 -1
View File
@@ -143,7 +143,7 @@ export class StateService implements IStateService {
}
getItem<T>(key: string, defaultValue: T): T;
getItem<T>(key: string, defaultValue: T | undefined): T | undefined;
getItem<T>(key: string, defaultValue?: T): T | undefined;
getItem<T>(key: string, defaultValue?: T): T | undefined {
return this.fileStorage.getItem(key, defaultValue);
}
@@ -5,6 +5,7 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
@@ -32,7 +33,7 @@ export class ExtensionsStorageSyncService extends Disposable implements IExtensi
declare readonly _serviceBrand: undefined;
private static toKey(extension: IExtensionIdWithVersion): string {
return `extensionKeys/${extension.id}@${extension.version}`;
return `extensionKeys/${adoptToGalleryExtensionId(extension.id)}@${extension.version}`;
}
private static fromKey(key: string): IExtensionIdWithVersion | undefined {
@@ -11,7 +11,7 @@ import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { areSameExtensions, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
@@ -73,6 +73,15 @@ async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagemen
return extensions;
}
function getExtensionStorageState(publisher: string, name: string, storageService: IStorageService): IStringDictionary<any> {
const extensionStorageValue = storageService.get(getExtensionId(publisher, name) /* use the same id used in extension host */, StorageScope.GLOBAL) || '{}';
return JSON.parse(extensionStorageValue);
}
function storeExtensionStorageState(publisher: string, name: string, extensionState: IStringDictionary<any>, storageService: IStorageService): void {
storageService.store(getExtensionId(publisher, name) /* use the same id used in extension host */, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` });
@@ -99,7 +108,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
@IIgnoredExtensionsManagementService private readonly extensionSyncManagementService: IIgnoredExtensionsManagementService,
@IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IConfigurationService configurationService: IConfigurationService,
@@ -125,7 +134,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
if (remoteExtensions) {
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`);
@@ -209,7 +218,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions);
const { added, removed, updated, remote } = mergeResult;
return {
@@ -225,7 +234,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
if (remoteExtensions !== null) {
const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions);
@@ -285,7 +294,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
async resolveContent(uri: URI): Promise<string | null> {
if (this.extUri.isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
const installedExtensions = await this.extensionManagementService.getInstalled();
const ignoredExtensions = this.extensionSyncManagementService.getIgnoredExtensions(installedExtensions);
const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions);
const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier)));
return this.format(localExtensions);
}
@@ -363,7 +372,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
// Builtin Extension Sync: Enablement & State
if (installedExtension && installedExtension.isBuiltin) {
if (e.state && installedExtension.manifest.version === e.version) {
this.updateExtensionState(e.state, e.identifier.id, installedExtension.manifest.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);
@@ -382,14 +391,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier);
/* Update extension state only if
* extension is installed and version is same as synced version or
* extension is not installed and installable
* extension is installed and version is same as synced version or
* extension is not installed and installable
*/
if (e.state &&
(installedExtension ? installedExtension.manifest.version === e.version /* Installed and has same version */
: !!extension /* Installable */)
) {
this.updateExtensionState(e.state, e.identifier.id, installedExtension?.manifest.version);
const publisher = installedExtension ? installedExtension.manifest.publisher : extension!.publisher;
const name = installedExtension ? installedExtension.manifest.name : extension!.name;
this.updateExtensionState(e.state, publisher, name, installedExtension?.manifest.version);
}
if (extension) {
@@ -436,15 +447,15 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return newSkippedExtensions;
}
private updateExtensionState(state: IStringDictionary<any>, id: string, version?: string): void {
const extensionState = JSON.parse(this.storageService.get(id, StorageScope.GLOBAL) || '{}');
const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id, version }) : undefined;
private updateExtensionState(state: IStringDictionary<any>, publisher: string, name: string, version: string | undefined): void {
const extensionState = getExtensionStorageState(publisher, name, this.storageService);
const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id: getGalleryExtensionId(publisher, name), version }) : undefined;
if (keys) {
keys.forEach(key => extensionState[key] = state[key]);
} else {
forEach(state, ({ key, value }) => extensionState[key] = value);
}
this.storageService.store(id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE);
storeExtensionStorageState(publisher, name, extensionState, this.storageService);
}
private parseExtensions(syncData: ISyncData): ISyncExtension[] {
@@ -465,8 +476,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
try {
const keys = this.extensionsStorageSyncService.getKeysForSync({ id: identifier.id, version: manifest.version });
if (keys) {
const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL) || '{}';
const extensionStorageState = JSON.parse(extensionStorageValue);
const extensionStorageState = getExtensionStorageState(manifest.publisher, manifest.name, this.storageService);
syncExntesion.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary<any>, key) => {
if (keys.includes(key)) {
state[key] = extensionStorageState[key];
@@ -490,6 +500,7 @@ export class ExtensionsInitializer extends AbstractInitializer {
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
@IStorageService private readonly storageService: IStorageService,
@IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@@ -511,12 +522,18 @@ export class ExtensionsInitializer extends AbstractInitializer {
const newlyEnabledExtensions: ILocalExtension[] = [];
const installedExtensions = await this.extensionManagementService.getInstalled();
const newExtensionsToSync = new Map<string, ISyncExtension>();
const installedExtensionsToSync: ISyncExtension[] = [];
const installedExtensionsToSync: { syncExtension: ISyncExtension, installedExtension: ILocalExtension }[] = [];
const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] };
const toDisable: IExtensionIdentifier[] = [];
for (const extension of remoteExtensions) {
if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) {
installedExtensionsToSync.push(extension);
if (this.ignoredExtensionsManagementService.hasToNeverSyncExtension(extension.identifier.id)) {
// Skip extension ignored to sync
continue;
}
const installedExtension = installedExtensions.find(i => areSameExtensions(i.identifier, extension.identifier));
if (installedExtension) {
installedExtensionsToSync.push({ syncExtension: extension, installedExtension });
if (extension.disabled) {
toDisable.push(extension.identifier);
}
@@ -536,11 +553,11 @@ export class ExtensionsInitializer extends AbstractInitializer {
}
// 1. Initialise already installed extensions state
for (const extensionToSync of installedExtensionsToSync) {
if (extensionToSync.state) {
const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}');
forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value);
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE);
for (const { syncExtension, installedExtension } of installedExtensionsToSync) {
if (syncExtension.state) {
const extensionState = getExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, this.storageService);
forEach(syncExtension.state, ({ key, value }) => extensionState[key] = value);
storeExtensionStorageState(installedExtension.manifest.publisher, installedExtension.manifest.name, extensionState, this.storageService);
}
}
@@ -560,7 +577,7 @@ export class ExtensionsInitializer extends AbstractInitializer {
try {
const extensionToSync = newExtensionsToSync.get(galleryExtension.identifier.id.toLowerCase())!;
if (extensionToSync.state) {
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL, StorageTarget.MACHINE);
storeExtensionStorageState(galleryExtension.publisher, galleryExtension.name, extensionToSync.state, this.storageService);
}
this.logService.trace(`Installing extension...`, galleryExtension.identifier.id);
const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */);
@@ -25,7 +25,8 @@ export namespace WebviewResourceResponse {
constructor(
public readonly stream: VSBufferReadableStream,
public readonly mimeType: string
public readonly etag: string | undefined,
public readonly mimeType: string,
) { }
}
@@ -36,7 +37,7 @@ export namespace WebviewResourceResponse {
}
interface FileReader {
readFileStream(resource: URI): Promise<VSBufferReadableStream>;
readFileStream(resource: URI): Promise<{ stream: VSBufferReadableStream, etag?: string }>;
}
export async function loadLocalResource(
@@ -73,7 +74,7 @@ export async function loadLocalResource(
logService.debug(`loadLocalResource - Loaded over http(s). requestUri=${requestUri}, response=${response.res.statusCode}`);
if (response.res.statusCode === 200) {
return new WebviewResourceResponse.StreamSuccess(response.stream, mime);
return new WebviewResourceResponse.StreamSuccess(response.stream, undefined, mime);
}
return WebviewResourceResponse.Failed;
}
@@ -82,7 +83,7 @@ export async function loadLocalResource(
const contents = await fileReader.readFileStream(resourceToLoad);
logService.debug(`loadLocalResource - Loaded using fileReader. requestUri=${requestUri}`);
return new WebviewResourceResponse.StreamSuccess(contents, mime);
return new WebviewResourceResponse.StreamSuccess(contents.stream, contents.etag, mime);
} catch (err) {
logService.debug(`loadLocalResource - Error using fileReader. requestUri=${requestUri}`);
console.log(err);
@@ -175,10 +175,14 @@ export class WebviewProtocolProvider extends Disposable {
};
}
const fileService = {
readFileStream: async (resource: URI): Promise<VSBufferReadableStream> => {
const fileReader = {
readFileStream: async (resource: URI): Promise<{ stream: VSBufferReadableStream, etag?: string }> => {
if (resource.scheme === Schemas.file) {
return (await this.fileService.readFileStream(resource)).value;
const result = (await this.fileService.readFileStream(resource));
return {
stream: result.value,
etag: result.etag
};
}
// Unknown uri scheme. Try delegating the file read back to the renderer
@@ -201,7 +205,7 @@ export class WebviewProtocolProvider extends Disposable {
throw new FileOperationError('Could not read file', FileOperationResult.FILE_NOT_FOUND);
}
return bufferToStream(result);
return { stream: bufferToStream(result), etag: undefined };
}
};
@@ -210,15 +214,41 @@ export class WebviewProtocolProvider extends Disposable {
roots: metadata.localResourceRoots,
remoteConnectionData: metadata.remoteConnectionData,
rewriteUri,
}, fileService, this.requestService, this.logService);
}, fileReader, this.requestService, this.logService);
if (result.type === WebviewResourceResponse.Type.Success) {
const cacheHeaders: Record<string, string> = result.etag ? {
'ETag': result.etag,
'Cache-Control': 'no-cache'
} : {};
const ifNoneMatch = request.headers['If-None-Match'];
if (ifNoneMatch && result.etag === ifNoneMatch) {
/*
* Note that the server generating a 304 response MUST
* generate any of the following header fields that would
* have been sent in a 200 (OK) response to the same request:
* Cache-Control, Content-Location, Date, ETag, Expires, and Vary.
* (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
*/
return callback({
statusCode: 304, // not modified
data: undefined, // The request fails if `data` is not set
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
...cacheHeaders
}
});
}
return callback({
statusCode: 200,
data: this.streamToNodeReadable(result.stream),
headers: {
'Content-Type': result.mimeType,
'Access-Control-Allow-Origin': '*',
...cacheHeaders
}
});
}
+52 -1
View File
@@ -4697,6 +4697,10 @@ declare module 'vscode' {
* This rule will only execute if the text after the cursor matches this regular expression.
*/
afterText?: RegExp;
/**
* This rule will only execute if the text above the current line matches this regular expression.
*/
previousLineText?: RegExp;
/**
* The action to execute.
*/
@@ -5821,6 +5825,11 @@ declare module 'vscode' {
setKeysForSync(keys: string[]): void;
};
/**
* A storage utility for secrets.
*/
readonly secrets: SecretStorage;
/**
* The uri of the directory containing the extension.
*/
@@ -5958,6 +5967,48 @@ declare module 'vscode' {
update(key: string, value: any): Thenable<void>;
}
/**
* The event data that is fired when a secret is added or removed.
*/
export interface SecretStorageChangeEvent {
/**
* The key of the secret that has changed.
*/
readonly key: string;
}
/**
* Represents a storage utility for secrets, information that is
* sensitive.
*/
export interface SecretStorage {
/**
* Retrieve a secret that was stored with key. Returns undefined if there
* is no password matching that key.
* @param key The key the secret was stored under.
* @returns The stored value or `undefined`.
*/
get(key: string): Thenable<string | undefined>;
/**
* Store a secret under a given key.
* @param key The key to store the secret under.
* @param value The secret.
*/
store(key: string, value: string): Thenable<void>;
/**
* Remove a secret from storage.
* @param key The key the secret was stored under.
*/
delete(key: string): Thenable<void>;
/**
* Fires when a secret is stored or deleted.
*/
onDidChange: Event<SecretStorageChangeEvent>;
}
/**
* Represents a color theme kind.
*/
@@ -12161,7 +12212,7 @@ declare module 'vscode' {
/**
* Optional reaction handler for creating and deleting reactions on a [comment](#Comment).
*/
reactionHandler?: (comment: Comment, reaction: CommentReaction) => Promise<void>;
reactionHandler?: (comment: Comment, reaction: CommentReaction) => Thenable<void>;
/**
* Dispose this comment controller.
+34 -66
View File
@@ -881,15 +881,6 @@ declare module 'vscode' {
//#endregion
//#region https://github.com/microsoft/vscode/issues/58440
export interface OnEnterRule {
/**
* This rule will only execute if the text above the this line matches this regular expression.
*/
oneLineAboveText?: RegExp;
}
//#endregion
//#region Tree View: https://github.com/microsoft/vscode/issues/61313 @alexr00
export interface TreeView<T> extends Disposable {
reveal(element: T | undefined, options?: { select?: boolean, focus?: boolean, expand?: boolean | number; }): Thenable<void>;
@@ -1577,16 +1568,16 @@ declare module 'vscode' {
* resolve the raw content for `uri` as the resouce is not necessarily a file on disk.
*/
// eslint-disable-next-line vscode-dts-provider-naming
openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise<NotebookData>;
openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Thenable<NotebookData>;
// eslint-disable-next-line vscode-dts-provider-naming
// eslint-disable-next-line vscode-dts-cancellation
resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise<void>;
resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Thenable<void>;
// eslint-disable-next-line vscode-dts-provider-naming
saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise<void>;
saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Thenable<void>;
// eslint-disable-next-line vscode-dts-provider-naming
saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise<void>;
saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Thenable<void>;
// eslint-disable-next-line vscode-dts-provider-naming
backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise<NotebookDocumentBackup>;
backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Thenable<NotebookDocumentBackup>;
}
export interface NotebookKernel {
@@ -1685,7 +1676,7 @@ declare module 'vscode' {
): Disposable;
export function createNotebookEditorDecorationType(options: NotebookDecorationRenderOptions): NotebookEditorDecorationType;
export function openNotebookDocument(uri: Uri, viewType?: string): Promise<NotebookDocument>;
export function openNotebookDocument(uri: Uri, viewType?: string): Thenable<NotebookDocument>;
export const onDidOpenNotebookDocument: Event<NotebookDocument>;
export const onDidCloseNotebookDocument: Event<NotebookDocument>;
export const onDidSaveNotebookDocument: Event<NotebookDocument>;
@@ -1729,7 +1720,7 @@ declare module 'vscode' {
export const onDidChangeActiveNotebookEditor: Event<NotebookEditor | undefined>;
export const onDidChangeNotebookEditorSelection: Event<NotebookEditorSelectionChangeEvent>;
export const onDidChangeNotebookEditorVisibleRanges: Event<NotebookEditorVisibleRangesChangeEvent>;
export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Promise<NotebookEditor>;
export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable<NotebookEditor>;
}
//#endregion
@@ -1940,7 +1931,7 @@ declare module 'vscode' {
}
export namespace languages {
export function getTokenInformationAtPosition(document: TextDocument, position: Position): Promise<TokenInformation>;
export function getTokenInformationAtPosition(document: TextDocument, position: Position): Thenable<TokenInformation>;
}
//#endregion
@@ -2367,13 +2358,6 @@ declare module 'vscode' {
location?: Location;
}
/**
* Additional metadata about the uri being opened
*/
interface OpenExternalUriContext {
}
//#endregion
//#region Opener service (https://github.com/microsoft/vscode/issues/109277)
@@ -2497,54 +2481,38 @@ declare module 'vscode' {
export function registerExternalUriOpener(id: string, schemes: readonly string[], opener: ExternalUriOpener, metadata: ExternalUriOpenerMetadata): Disposable;
}
//#endregion
//#region https://github.com/microsoft/vscode/issues/112249
/**
* The event data that is fired when a secret is added or removed.
*/
export interface SecretStorageChangeEvent {
interface OpenExternalOptions {
/**
* The key of the secret that has changed.
*
* If `true`, then VS Code will check if any contributed openers can handle the
* uri, and fallback to the default opener behavior.
*
* If it is string, then this specifies the id of the `ExternalUriOpener`
* that should be used if it is available. Use `'default'` to force VS Code's
* standard external opener to be used.
*/
key: string;
readonly allowContributedOpeners?: boolean | string;
}
/**
* Represents a storage utility for secrets, information that is
* sensitive.
*/
export interface SecretStorage {
/**
* Retrieve a secret that was stored with key. Returns undefined if there
* is no password matching that key.
* @param key The key the password was stored under.
* @returns The stored value or `undefined`.
*/
get(key: string): Thenable<string | undefined>;
/**
* Store a secret under a given key.
* @param key The key to store the password under.
* @param value The password.
*/
set(key: string, value: string): Thenable<void>;
/**
* Remove a secret from storage.
* @param key The key the password was stored under.
*/
delete(key: string): Thenable<void>;
/**
* Fires when a secret is set or deleted.
*/
onDidChange: Event<SecretStorageChangeEvent>;
namespace env {
export function openExternal(target: Uri, options?: OpenExternalOptions): Thenable<boolean>;
}
export interface ExtensionContext {
secrets: SecretStorage;
//#endregionn
//#region https://github.com/Microsoft/vscode/issues/15178
// TODO@API must be a class
export interface OpenEditorInfo {
name: string;
resource: Uri;
}
export namespace window {
export const openEditors: ReadonlyArray<OpenEditorInfo>;
// todo@API proper event type
export const onDidChangeOpenEditors: Event<void>;
}
//#endregion
@@ -3,13 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostContext, IExtHostEditorTabsShape, IExtHostContext, MainContext, IEditorTabDto } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { Verbosity } from 'vs/workbench/common/editor';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { GroupChangeKind, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
export interface ITabInfo {
name: string;
@@ -19,34 +18,62 @@ export interface ITabInfo {
@extHostNamedCustomer(MainContext.MainThreadEditorTabs)
export class MainThreadEditorTabs {
private readonly _registration: IDisposable;
private static _GroupEventFilter = new Set([GroupChangeKind.EDITOR_CLOSE, GroupChangeKind.EDITOR_OPEN]);
private readonly _dispoables = new DisposableStore();
private readonly _groups = new Map<IEditorGroup, IDisposable>();
private readonly _proxy: IExtHostEditorTabsShape;
constructor(
_extHostContext: IExtHostContext,
extHostContext: IExtHostContext,
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
) {
this._registration = CommandsRegistry.registerCommand('_textEditorTabs', () => {
return this._fetchTextEditors();
});
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs);
this._editorGroupsService.groups.forEach(this._subscribeToGroup, this);
this._dispoables.add(_editorGroupsService.onDidAddGroup(this._subscribeToGroup, this));
this._dispoables.add(_editorGroupsService.onDidRemoveGroup(e => {
const subscription = this._groups.get(e);
if (subscription) {
subscription.dispose();
this._groups.delete(e);
this._pushEditorTabs();
}
}));
this._pushEditorTabs();
}
dispose(): void {
this._registration.dispose();
dispose(this._groups.values());
this._dispoables.dispose();
}
private _fetchTextEditors(): ITabInfo[] {
const result: ITabInfo[] = [];
private _subscribeToGroup(group: IEditorGroup) {
this._groups.get(group)?.dispose();
const listener = group.onDidGroupChange(e => {
if (MainThreadEditorTabs._GroupEventFilter.has(e.kind)) {
this._pushEditorTabs();
}
});
this._groups.set(group, listener);
}
private _pushEditorTabs(): void {
const tabs: IEditorTabDto[] = [];
for (const group of this._editorGroupsService.groups) {
for (const editor of group.editors) {
if (editor.isDisposed() || !editor.resource) {
continue;
}
result.push({
tabs.push({
group: group.id,
name: editor.getTitle(Verbosity.SHORT) ?? '',
resource: editor.resource
});
}
}
return result;
this._proxy.$acceptEditorTabs(tabs);
}
}
@@ -688,7 +688,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
return {
beforeText: MainThreadLanguageFeatures._reviveRegExp(onEnterRule.beforeText),
afterText: onEnterRule.afterText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.afterText) : undefined,
oneLineAboveText: onEnterRule.oneLineAboveText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.oneLineAboveText) : undefined,
previousLineText: onEnterRule.previousLineText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.previousLineText) : undefined,
action: onEnterRule.action
};
}
@@ -3,13 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
import { URI, UriComponents } from 'vs/base/common/uri';
import { CancellationToken } from 'vs/base/common/cancellation';
const reviveDiff = (diff: TestsDiff) => {
for (const entry of diff) {
@@ -41,6 +41,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting);
this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri)));
this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri)));
testService.updateRootProviderCount(1);
for (const { resource, uri } of this.testService.subscriptions) {
this.proxy.$subscribeToTests(resource, uri);
@@ -64,13 +65,6 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
this.testService.unregisterTestController(id);
}
/**
* @inheritdoc
*/
$updateDiscoveringCount(resource: ExtHostTestingResource, uriComponents: UriComponents, delta: number): void {
this.testService.updateDiscoveringCount(resource, URI.revive(uriComponents), delta);
}
/**
* @inheritdoc
*/
@@ -103,6 +97,10 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
}
public dispose() {
// no-op
this.testService.updateRootProviderCount(-1);
for (const subscription of this.testSubscriptions.values()) {
subscription.dispose();
}
this.testSubscriptions.clear();
}
}
@@ -14,6 +14,7 @@ import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorit
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { PORT_AUTO_FORWARD_SETTING } from 'vs/workbench/contrib/remote/browser/tunnelView';
import { ILogService } from 'vs/platform/log/common/log';
@extHostNamedCustomer(MainContext.MainThreadTunnelService)
export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape {
@@ -25,7 +26,8 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
@ITunnelService private readonly tunnelService: ITunnelService,
@INotificationService private readonly notificationService: INotificationService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IConfigurationService private readonly configurationService: IConfigurationService,
@ILogService private readonly logService: ILogService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTunnelService);
@@ -100,6 +102,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
const forward = this._proxy.$forwardPort(tunnelOptions, tunnelCreationOptions);
if (forward) {
return forward.then(tunnel => {
this.logService.trace(`MainThreadTunnelService: New tunnel established by tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`);
if (!tunnel) {
return undefined;
}
@@ -110,6 +113,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
tunnelLocalPort: typeof tunnel.localAddress !== 'string' ? tunnel.localAddress.port : undefined,
public: tunnel.public,
dispose: async (silent?: boolean) => {
this.logService.trace(`MainThreadTunnelService: Closing tunnel from tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`);
return this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent);
}
};
@@ -78,7 +78,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void {
const webview = this.getWebview(handle);
if (this.isSupportedLink(webview, URI.parse(link))) {
this._openerService.open(link, { fromUserGesture: true });
this._openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true });
}
}
@@ -52,7 +52,11 @@ export class MainThreadWindow implements MainThreadWindowShape {
// called with URI or transformed -> use uri
target = uri;
}
return this.openerService.open(target, { openExternal: true, allowTunneling: options.allowTunneling });
return this.openerService.open(target, {
openExternal: true,
allowTunneling: options.allowTunneling,
allowContributedOpeners: options.allowContributedOpeners,
});
}
async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise<UriComponents> {
@@ -3,35 +3,32 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { coalesce } from 'vs/base/common/arrays';
import { forEach } from 'vs/base/common/collections';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as resources from 'vs/base/common/resources';
import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation, testViewIcon } from 'vs/workbench/common/views';
import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { coalesce, } from 'vs/base/common/arrays';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files';
import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm';
import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug';
import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { URI } from 'vs/base/common/uri';
import { ViewletRegistry, Extensions as ViewletExtensions, ShowViewletAction } from 'vs/workbench/browser/viewlet';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { localize } from 'vs/nls';
import { registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { Extensions as ViewletExtensions, ShowViewletAction2, ViewletRegistry } from 'vs/workbench/browser/viewlet';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { Extensions as ViewContainerExtensions, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views';
import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug';
import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files';
import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer';
import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm';
import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane';
import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
export interface IUserFriendlyViewsContainerDescriptor {
id: string;
@@ -255,7 +252,8 @@ const viewsExtensionPoint: IExtensionPoint<ViewExtensionPointType> = ExtensionsR
jsonSchema: viewsContribution
});
const TEST_VIEW_CONTAINER_ORDER = 6;
const CUSTOM_VIEWS_START_ORDER = 7;
class ViewsExtensionHandler implements IWorkbenchContribution {
private viewContainersRegistry: IViewContainersRegistry;
@@ -271,7 +269,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
}
private handleAndRegisterCustomViewContainers() {
this.registerTestViewContainer();
viewsContainersExtensionPoint.setHandler((extensions, { added, removed }) => {
if (removed.length) {
this.removeCustomViewContainers(removed);
@@ -284,7 +281,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser<ViewContainerExtensionPointType>[], existingViewContainers: ViewContainer[]): void {
const viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
let activityBarOrder = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length + 1;
let activityBarOrder = CUSTOM_VIEWS_START_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length;
let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1;
for (let { value, collector, description } of extensionPoints) {
forEach(value, entry => {
@@ -318,13 +315,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
}
}
private registerTestViewContainer(): void {
const title = localize('test', "Test");
const icon = testViewIcon;
this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined, ViewContainerLocation.Sidebar);
}
private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean {
if (!Array.isArray(viewsContainersDescriptors)) {
collector.error(localize('viewcontainer requirearray', "views containers must be an array"));
@@ -395,22 +385,15 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
}, location);
// Register Action to Open Viewlet
class OpenCustomViewletAction extends ShowViewletAction {
constructor(
id: string, label: string,
@IViewletService viewletService: IViewletService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService
) {
super(id, label, id, viewletService, editorGroupService, layoutService);
registerAction2(class OpenCustomViewletAction extends ShowViewletAction2 {
constructor() {
super({ id, f1: true, title: localize('showViewlet', "Show {0}", title), category: CATEGORIES.View.value });
}
}
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(
SyncActionDescriptor.create(OpenCustomViewletAction, id, localize('showViewlet', "Show {0}", title)),
`View: Show ${title}`,
CATEGORIES.View.value
);
protected viewletId() {
return id;
}
});
}
return viewContainer;
@@ -25,7 +25,7 @@ export class ExtHostSecretState implements ExtHostSecretStateShape {
return this._proxy.$getPassword(extensionId, key);
}
set(extensionId: string, key: string, value: string): Promise<void> {
store(extensionId: string, key: string, value: string): Promise<void> {
return this._proxy.$setPassword(extensionId, key, value);
}
@@ -84,6 +84,7 @@ import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSyste
import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting';
import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener';
import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState';
import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs';
export interface IExtensionApiFactory {
(extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode;
@@ -133,6 +134,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService));
// manually create and register addressable instances
const extHostEditorTabs = rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, new ExtHostEditorTabs());
const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol));
const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors));
const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService));
@@ -292,8 +294,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
get shell() {
return extHostTerminalService.getDefaultShell(false, configProvider);
},
openExternal(uri: URI) {
return extHostWindow.openUri(uri, { allowTunneling: !!initData.remote.authority });
openExternal(uri: URI, options?: { allowContributedOpeners?: boolean | string; }) {
return extHostWindow.openUri(uri, {
allowTunneling: !!initData.remote.authority,
allowContributedOpeners: options?.allowContributedOpeners,
});
},
asExternalUri(uri: URI) {
if (uri.scheme === initData.environment.appUriScheme) {
@@ -680,6 +685,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension);
return extHostUriOpeners.registerUriOpener(extension.identifier, id, schemes, opener, metadata);
},
get openEditors() {
checkProposedApiEnabled(extension);
return extHostEditorTabs.tabs;
},
get onDidChangeOpenEditors() {
checkProposedApiEnabled(extension);
return extHostEditorTabs.onDidChangeTabs;
}
};
// namespace: workspace
@@ -335,7 +335,7 @@ export interface IIndentationRuleDto {
export interface IOnEnterRuleDto {
beforeText: IRegExpDto;
afterText?: IRegExpDto;
oneLineAboveText?: IRegExpDto;
previousLineText?: IRegExpDto;
action: EnterAction;
}
export interface ILanguageConfigurationDto {
@@ -607,15 +607,29 @@ export interface MainThreadEditorInsetsShape extends IDisposable {
$postMessage(handle: number, value: any): Promise<boolean>;
}
export interface MainThreadEditorTabsShape extends IDisposable {
}
export interface ExtHostEditorInsetsShape {
$onDidDispose(handle: number): void;
$onDidReceiveMessage(handle: number, message: any): void;
}
//#region --- open editors model
export interface MainThreadEditorTabsShape extends IDisposable {
// manage tabs: move, close, rearrange etc
}
export interface IEditorTabDto {
group: number;
name: string;
resource: UriComponents
}
export interface IExtHostEditorTabsShape {
$acceptEditorTabs(tabs: IEditorTabDto[]): void;
}
//#endregion
export type WebviewHandle = string;
export interface WebviewPanelShowOptions {
@@ -980,6 +994,7 @@ export interface MainThreadDebugServiceShape extends IDisposable {
export interface IOpenUriOptions {
readonly allowTunneling?: boolean;
readonly allowContributedOpeners?: boolean | string;
}
export interface MainThreadWindowShape extends IDisposable {
@@ -1838,7 +1853,6 @@ export interface MainThreadTestingShape {
$unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
$publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void;
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<RunTestsResult>;
$updateDiscoveringCount(resource: ExtHostTestingResource, uri: UriComponents, delta: number): void;
}
// --- proxy identifiers
@@ -1927,6 +1941,7 @@ export const ExtHostContext = {
ExtHostCustomEditors: createExtId<ExtHostCustomEditorsShape>('ExtHostCustomEditors'),
ExtHostWebviewViews: createExtId<ExtHostWebviewViewsShape>('ExtHostWebviewViews'),
ExtHostEditorInsets: createExtId<ExtHostEditorInsetsShape>('ExtHostEditorInsets'),
ExtHostEditorTabs: createExtId<IExtHostEditorTabsShape>('ExtHostEditorTabs'),
ExtHostProgress: createMainId<ExtHostProgressShape>('ExtHostProgress'),
ExtHostComments: createMainId<ExtHostCommentsShape>('ExtHostComments'),
ExtHostSecretState: createMainId<ExtHostSecretStateShape>('ExtHostSecretState'),
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as vscode from 'vscode';
import { IEditorTabDto, IExtHostEditorTabsShape } from 'vs/workbench/api/common/extHost.protocol';
import { URI } from 'vs/base/common/uri';
import { Emitter, Event } from 'vs/base/common/event';
export interface IEditorTab {
name: string;
group: number;
resource: vscode.Uri
}
export class ExtHostEditorTabs implements IExtHostEditorTabsShape {
private readonly _onDidChangeTabs = new Emitter<void>();
readonly onDidChangeTabs: Event<void> = this._onDidChangeTabs.event;
private _tabs: IEditorTab[] = [];
get tabs(): readonly IEditorTab[] {
return this._tabs;
}
$acceptEditorTabs(tabs: IEditorTabDto[]): void {
this._tabs = tabs.map(dto => {
return {
name: dto.name,
group: dto.group,
resource: URI.revive(dto.resource)
};
});
this._onDidChangeTabs.fire();
}
}
@@ -1921,7 +1921,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
return {
beforeText: ExtHostLanguageFeatures._serializeRegExp(onEnterRule.beforeText),
afterText: onEnterRule.afterText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.afterText) : undefined,
oneLineAboveText: onEnterRule.oneLineAboveText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.oneLineAboveText) : undefined,
previousLineText: onEnterRule.previousLineText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.previousLineText) : undefined,
action: onEnterRule.action
};
}
@@ -33,8 +33,8 @@ export class ExtensionSecrets implements vscode.SecretStorage {
return this._secretState.get(this._id, key);
}
set(key: string, value: string): Promise<void> {
return this._secretState.set(this._id, key, value);
store(key: string, value: string): Promise<void> {
return this._secretState.store(this._id, key, value);
}
delete(key: string): Promise<void> {
+8 -17
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { mapFind } from 'vs/base/common/arrays';
import { disposableTimeout, RunOnceScheduler } from 'vs/base/common/async';
import { disposableTimeout } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
@@ -19,7 +19,7 @@ import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters';
import { Disposable, RequiredTestItem } from 'vs/workbench/api/common/extHostTypes';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import type * as vscode from 'vscode';
const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`;
@@ -125,19 +125,6 @@ export class ExtHostTesting implements ExtHostTestingShape {
return;
}
let delta = 0;
const updateCountScheduler = new RunOnceScheduler(() => {
if (delta !== 0) {
this.proxy.$updateDiscoveringCount(resource, uri, delta);
delta = 0;
}
}, 5);
const updateDelta = (amount: number) => {
delta += amount;
updateCountScheduler.schedule();
};
const subscribeFn = (id: string, provider: vscode.TestProvider) => {
try {
const hierarchy = method!(provider);
@@ -145,10 +132,10 @@ export class ExtHostTesting implements ExtHostTestingShape {
return;
}
updateDelta(1);
collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, 1]);
disposable.add(hierarchy);
collection.addRoot(hierarchy.root, id);
Promise.resolve(hierarchy.discoveredInitialTests).then(() => updateDelta(-1));
Promise.resolve(hierarchy.discoveredInitialTests).then(() => collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, -1]));
hierarchy.onDidChangeTest(e => collection.onItemChange(e, id));
} catch (e) {
console.error(e);
@@ -161,6 +148,10 @@ export class ExtHostTesting implements ExtHostTestingShape {
subscribeFn(id, provider);
}
// note: we don't increment the root count initially -- this is done by the
// main thread, incrementing once per extension host. We just push the
// diff to signal that roots have been discovered.
collection.pushDiff([TestDiffOpType.DeltaRootsComplete, -1]);
this.testSubscriptions.set(subscriptionKey, { store: disposable, collection, subscribeFn });
}
@@ -2315,9 +2315,6 @@ export class FunctionBreakpoint extends Breakpoint {
constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) {
super(enabled, condition, hitCondition, logMessage);
if (!functionName) {
throw illegalArgument('functionName');
}
this.functionName = functionName;
}
}
@@ -15,13 +15,13 @@ import * as fs from 'fs';
import * as pfs from 'vs/base/node/pfs';
import { isLinux } from 'vs/base/common/platform';
import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { asPromise } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { TunnelOptions, TunnelCreationOptions } from 'vs/platform/remote/common/tunnel';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { promisify } from 'util';
import { MovingAverage } from 'vs/base/common/numbers';
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { ILogService } from 'vs/platform/log/common/log';
class ExtensionTunnel implements vscode.Tunnel {
private _onDispose: Emitter<void> = new Emitter();
@@ -142,7 +142,8 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService
@IExtHostInitDataService initData: IExtHostInitDataService,
@ILogService private readonly logService: ILogService
) {
super();
this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService);
@@ -234,16 +235,19 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<TunnelDto | undefined> {
if (this._forwardPortProvider) {
const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions);
if (providedPort !== undefined) {
return asPromise(() => providedPort).then(tunnel => {
try {
const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions);
if (providedPort !== undefined) {
const tunnel = await providedPort;
if (!this._extensionTunnels.has(tunnelOptions.remoteAddress.host)) {
this._extensionTunnels.set(tunnelOptions.remoteAddress.host, new Map());
}
const disposeListener = this._register(tunnel.onDidDispose(() => this._proxy.$closeTunnel(tunnel.remoteAddress)));
this._extensionTunnels.get(tunnelOptions.remoteAddress.host)!.set(tunnelOptions.remoteAddress.port, { tunnel, disposeListener });
return Promise.resolve(TunnelDto.fromApiTunnel(tunnel));
});
return TunnelDto.fromApiTunnel(tunnel);
}
} catch (e) {
this.logService.trace('$forwardPort: tunnel provider error');
}
}
return undefined;
@@ -8,7 +8,7 @@ import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action
import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService';
import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IAction, Action, SubmenuAction, Separator, toAction } from 'vs/base/common/actions';
import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions';
import * as DOM from 'vs/base/browser/dom';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { isMacintosh, isWeb, isIOS, isNative } from 'vs/base/common/platform';
@@ -709,7 +709,14 @@ export class CustomMenubarControl extends MenubarControl {
const webNavigationActions: IAction[] = [];
const href = this.environmentService.options?.homeIndicator?.href;
if (href) {
webNavigationActions.push(toAction({ id: 'goHome', label: nls.localize('goHome', "Go Home"), run: () => window.location.href = href }));
webNavigationActions.push(new Action('goHome', nls.localize('goHome', "Go Home"), undefined, true,
async (event?: MouseEvent) => {
if ((!isMacintosh && event?.ctrlKey) || (isMacintosh && event?.metaKey)) {
window.open(href, '_blank');
} else {
window.location.href = href;
}
}));
}
const otherActions = this.getWebNavigationMenuItemActions().map(action => {
+36 -11
View File
@@ -9,7 +9,7 @@ import { Action } from 'vs/base/common/actions';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite';
import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation';
import { IConstructorSignature0, IInstantiationService, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -24,6 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { PaneComposite } from 'vs/workbench/browser/panecomposite';
import { Event } from 'vs/base/common/event';
import { FilterViewPaneContainer } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { Action2 } from 'vs/platform/actions/common/actions';
export abstract class Viewlet extends PaneComposite implements IViewlet {
@@ -137,7 +138,7 @@ export class ShowViewletAction extends Action {
async run(): Promise<void> {
// Pass focus to viewlet if not open or focused
if (this.otherViewletShowing() || !this.sidebarHasFocus()) {
if (otherViewletShowing(this.viewletService, this.viewletId) || !sidebarHasFocus(this.viewletService, this.layoutService)) {
await this.viewletService.openViewlet(this.viewletId, true);
return;
}
@@ -145,18 +146,42 @@ export class ShowViewletAction extends Action {
// Otherwise pass focus to editor group
this.editorGroupService.activeGroup.focus();
}
}
private otherViewletShowing(): boolean {
const activeViewlet = this.viewletService.getActiveViewlet();
/**
* A reusable action to show a viewlet with a specific id.
*/
export abstract class ShowViewletAction2 extends Action2 {
/**
* Gets the viewlet ID to show.
*/
protected abstract viewletId(): string;
return !activeViewlet || activeViewlet.getId() !== this.viewletId;
}
public async run(accessor: ServicesAccessor): Promise<void> {
const viewletService = accessor.get(IViewletService);
const editorGroupService = accessor.get(IEditorGroupsService);
const layoutService = accessor.get(IWorkbenchLayoutService);
private sidebarHasFocus(): boolean {
const activeViewlet = this.viewletService.getActiveViewlet();
const activeElement = document.activeElement;
const sidebarPart = this.layoutService.getContainer(Parts.SIDEBAR_PART);
// Pass focus to viewlet if not open or focused
if (otherViewletShowing(viewletService, this.viewletId()) || !sidebarHasFocus(viewletService, layoutService)) {
await viewletService.openViewlet(this.viewletId(), true);
return;
}
return !!(activeViewlet && activeElement && sidebarPart && DOM.isAncestor(activeElement, sidebarPart));
// Otherwise pass focus to editor group
editorGroupService.activeGroup.focus();
}
}
const otherViewletShowing = (viewletService: IViewletService, viewletId: string): boolean => {
const activeViewlet = viewletService.getActiveViewlet();
return !activeViewlet || activeViewlet.getId() !== viewletId;
};
const sidebarHasFocus = (viewletService: IViewletService, layoutService: IWorkbenchLayoutService): boolean => {
const activeViewlet = viewletService.getActiveViewlet();
const activeElement = document.activeElement;
const sidebarPart = layoutService.getContainer(Parts.SIDEBAR_PART);
return !!(activeViewlet && activeElement && sidebarPart && DOM.isAncestor(activeElement, sidebarPart));
};
+1 -1
View File
@@ -19,7 +19,7 @@ export interface IComposite {
readonly onDidBlur: Event<void>;
/**
* Returns true if the composite has focus.
* Returns true if the composite has focus.
*/
hasFocus(): boolean;
-3
View File
@@ -29,9 +29,6 @@ import { Codicon } from 'vs/base/common/codicons';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { CancellationToken } from 'vs/base/common/cancellation';
export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test';
export const testViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.'));
export const defaultViewIcon = registerIcon('default-view-icon', Codicon.window, localize('defaultViewIcon', 'Default view icon.'));
export namespace Extensions {
@@ -409,14 +409,14 @@ export class CategoryElementRenderer implements ITreeRenderer<CategoryElement, F
} else if (URI.isUri(metadata.iconPath)) {
// background-image
template.icon.className = 'uri-icon';
template.icon.style.setProperty('--background-dark', `url("${metadata.iconPath.toString(true)}")`);
template.icon.style.setProperty('--background-light', `url("${metadata.iconPath.toString(true)}")`);
template.icon.style.setProperty('--background-dark', dom.asCSSUrl(metadata.iconPath));
template.icon.style.setProperty('--background-light', dom.asCSSUrl(metadata.iconPath));
} else if (metadata.iconPath) {
// background-image
template.icon.className = 'uri-icon';
template.icon.style.setProperty('--background-dark', `url("${metadata.iconPath.dark.toString(true)}")`);
template.icon.style.setProperty('--background-light', `url("${metadata.iconPath.light.toString(true)}")`);
template.icon.style.setProperty('--background-dark', dom.asCSSUrl(metadata.iconPath.dark));
template.icon.style.setProperty('--background-light', dom.asCSSUrl(metadata.iconPath.light));
}
template.label.setLabel(metadata.label, metadata.description, {
@@ -608,14 +608,14 @@ class TextEditElementTemplate {
} else if (URI.isUri(iconPath)) {
// background-image
this._icon.className = 'uri-icon';
this._icon.style.setProperty('--background-dark', `url("${iconPath.toString(true)}")`);
this._icon.style.setProperty('--background-light', `url("${iconPath.toString(true)}")`);
this._icon.style.setProperty('--background-dark', dom.asCSSUrl(iconPath));
this._icon.style.setProperty('--background-light', dom.asCSSUrl(iconPath));
} else {
// background-image
this._icon.className = 'uri-icon';
this._icon.style.setProperty('--background-dark', `url("${iconPath.dark.toString(true)}")`);
this._icon.style.setProperty('--background-light', `url("${iconPath.light.toString(true)}")`);
this._icon.style.setProperty('--background-dark', dom.asCSSUrl(iconPath.dark));
this._icon.style.setProperty('--background-light', dom.asCSSUrl(iconPath.light));
}
}
@@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as types from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { LanguageIdentifier } from 'vs/editor/common/modes';
import { CharacterPair, CommentRule, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration';
import { CharacterPair, CommentRule, EnterAction, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, LanguageConfiguration, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { IModeService } from 'vs/editor/common/services/modeService';
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
@@ -31,6 +31,19 @@ interface IIndentationRules {
unIndentedLinePattern?: string | IRegExp;
}
interface IEnterAction {
indent: 'none' | 'indent' | 'indentOutdent' | 'outdent';
appendText?: string;
removeText?: number;
}
interface IOnEnterRule {
beforeText: string | IRegExp;
afterText?: string | IRegExp;
previousLineText?: string | IRegExp;
action: IEnterAction;
}
interface ILanguageConfiguration {
comments?: CommentRule;
brackets?: CharacterPair[];
@@ -40,6 +53,7 @@ interface ILanguageConfiguration {
indentationRules?: IIndentationRules;
folding?: FoldingRules;
autoCloseBefore?: string;
onEnterRules?: IOnEnterRule[];
}
function isStringArr(something: string[] | null): something is string[] {
@@ -93,7 +107,7 @@ export class LanguageConfigurationFileHandler {
}
this._done[languageIdentifier.id] = true;
let configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language);
const configurationFiles = this._modeService.getConfigurationFiles(languageIdentifier.language);
configurationFiles.forEach((configFileLocation) => this._handleConfigFile(languageIdentifier, configFileLocation));
}
@@ -254,18 +268,82 @@ export class LanguageConfigurationFileHandler {
return result;
}
// private _mapCharacterPairs(pairs: Array<CharacterPair | IAutoClosingPairConditional>): IAutoClosingPairConditional[] {
// return pairs.map(pair => {
// if (Array.isArray(pair)) {
// return { open: pair[0], close: pair[1] };
// }
// return <IAutoClosingPairConditional>pair;
// });
// }
private _extractValidOnEnterRules(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): OnEnterRule[] | null {
const source = configuration.onEnterRules;
if (typeof source === 'undefined') {
return null;
}
if (!Array.isArray(source)) {
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules\` to be an array.`);
return null;
}
let result: OnEnterRule[] | null = null;
for (let i = 0, len = source.length; i < len; i++) {
const onEnterRule = source[i];
if (!types.isObject(onEnterRule)) {
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}]\` to be an object.`);
continue;
}
if (!types.isObject(onEnterRule.action)) {
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action\` to be an object.`);
continue;
}
let indentAction: IndentAction;
if (onEnterRule.action.indent === 'none') {
indentAction = IndentAction.None;
} else if (onEnterRule.action.indent === 'indent') {
indentAction = IndentAction.Indent;
} else if (onEnterRule.action.indent === 'indentOutdent') {
indentAction = IndentAction.IndentOutdent;
} else if (onEnterRule.action.indent === 'outdent') {
indentAction = IndentAction.Outdent;
} else {
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.indent\` to be 'none', 'indent', 'indentOutdent' or 'outdent'.`);
continue;
}
const action: EnterAction = { indentAction };
if (onEnterRule.action.appendText) {
if (typeof onEnterRule.action.appendText === 'string') {
action.appendText = onEnterRule.action.appendText;
} else {
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.appendText\` to be undefined or a string.`);
}
}
if (onEnterRule.action.removeText) {
if (typeof onEnterRule.action.removeText === 'number') {
action.removeText = onEnterRule.action.removeText;
} else {
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`onEnterRules[${i}].action.removeText\` to be undefined or a number.`);
}
}
const beforeText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].beforeText`, onEnterRule.beforeText);
if (!beforeText) {
continue;
}
const resultingOnEnterRule: OnEnterRule = { beforeText, action };
if (onEnterRule.afterText) {
const afterText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].afterText`, onEnterRule.afterText);
if (afterText) {
resultingOnEnterRule.afterText = afterText;
}
}
if (onEnterRule.previousLineText) {
const previousLineText = this._parseRegex(languageIdentifier, `onEnterRules[${i}].previousLineText`, onEnterRule.previousLineText);
if (previousLineText) {
resultingOnEnterRule.previousLineText = previousLineText;
}
}
result = result || [];
result.push(resultingOnEnterRule);
}
return result;
}
private _handleConfig(languageIdentifier: LanguageIdentifier, configuration: ILanguageConfiguration): void {
let richEditConfig: LanguageConfiguration = {};
const richEditConfig: LanguageConfiguration = {};
const comments = this._extractValidCommentRule(languageIdentifier, configuration);
if (comments) {
@@ -293,25 +371,21 @@ export class LanguageConfigurationFileHandler {
}
if (configuration.wordPattern) {
try {
let wordPattern = this._parseRegex(configuration.wordPattern);
if (wordPattern) {
richEditConfig.wordPattern = wordPattern;
}
} catch (error) {
// Malformed regexes are ignored
const wordPattern = this._parseRegex(languageIdentifier, `wordPattern`, configuration.wordPattern);
if (wordPattern) {
richEditConfig.wordPattern = wordPattern;
}
}
if (configuration.indentationRules) {
let indentationRules = this._mapIndentationRules(configuration.indentationRules);
const indentationRules = this._mapIndentationRules(languageIdentifier, configuration.indentationRules);
if (indentationRules) {
richEditConfig.indentationRules = indentationRules;
}
}
if (configuration.folding) {
let markers = configuration.folding.markers;
const markers = configuration.folding.markers;
richEditConfig.folding = {
offSide: configuration.folding.offSide,
@@ -319,44 +393,66 @@ export class LanguageConfigurationFileHandler {
};
}
const onEnterRules = this._extractValidOnEnterRules(languageIdentifier, configuration);
if (onEnterRules) {
richEditConfig.onEnterRules = onEnterRules;
}
LanguageConfigurationRegistry.register(languageIdentifier, richEditConfig);
}
private _parseRegex(value: string | IRegExp) {
private _parseRegex(languageIdentifier: LanguageIdentifier, confPath: string, value: string | IRegExp) {
if (typeof value === 'string') {
return new RegExp(value, '');
} else if (typeof value === 'object') {
return new RegExp(value.pattern, value.flags);
try {
return new RegExp(value, '');
} catch (err) {
console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err);
return null;
}
}
if (types.isObject(value)) {
if (typeof value.pattern !== 'string') {
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.pattern\` to be a string.`);
return null;
}
if (typeof value.flags !== 'undefined' && typeof value.flags !== 'string') {
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}.flags\` to be a string.`);
return null;
}
try {
return new RegExp(value.pattern, value.flags);
} catch (err) {
console.warn(`[${languageIdentifier.language}]: Invalid regular expression in \`${confPath}\`: `, err);
return null;
}
}
console.warn(`[${languageIdentifier.language}]: language configuration: expected \`${confPath}\` to be a string or an object.`);
return null;
}
private _mapIndentationRules(indentationRules: IIndentationRules): IndentationRule | null {
try {
let increaseIndentPattern = this._parseRegex(indentationRules.increaseIndentPattern);
let decreaseIndentPattern = this._parseRegex(indentationRules.decreaseIndentPattern);
if (increaseIndentPattern && decreaseIndentPattern) {
let result: IndentationRule = {
increaseIndentPattern: increaseIndentPattern,
decreaseIndentPattern: decreaseIndentPattern
};
if (indentationRules.indentNextLinePattern) {
result.indentNextLinePattern = this._parseRegex(indentationRules.indentNextLinePattern);
}
if (indentationRules.unIndentedLinePattern) {
result.unIndentedLinePattern = this._parseRegex(indentationRules.unIndentedLinePattern);
}
return result;
}
} catch (error) {
// Malformed regexes are ignored
private _mapIndentationRules(languageIdentifier: LanguageIdentifier, indentationRules: IIndentationRules): IndentationRule | null {
const increaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.increaseIndentPattern`, indentationRules.increaseIndentPattern);
if (!increaseIndentPattern) {
return null;
}
const decreaseIndentPattern = this._parseRegex(languageIdentifier, `indentationRules.decreaseIndentPattern`, indentationRules.decreaseIndentPattern);
if (!decreaseIndentPattern) {
return null;
}
return null;
const result: IndentationRule = {
increaseIndentPattern: increaseIndentPattern,
decreaseIndentPattern: decreaseIndentPattern
};
if (indentationRules.indentNextLinePattern) {
result.indentNextLinePattern = this._parseRegex(languageIdentifier, `indentationRules.indentNextLinePattern`, indentationRules.indentNextLinePattern);
}
if (indentationRules.unIndentedLinePattern) {
result.unIndentedLinePattern = this._parseRegex(languageIdentifier, `indentationRules.unIndentedLinePattern`, indentationRules.unIndentedLinePattern);
}
return result;
}
}
@@ -601,6 +697,100 @@ const schema: IJSONSchema = {
}
}
}
},
onEnterRules: {
type: 'array',
items: {
type: 'object',
description: nls.localize('schema.onEnterRules', 'The language\'s rules to be evaluated when pressing Enter.'),
required: ['beforeText', 'action'],
properties: {
beforeText: {
type: ['string', 'object'],
description: nls.localize('schema.onEnterRules.beforeText', 'This rule will only execute if the text before the cursor matches this regular expression.'),
properties: {
pattern: {
type: 'string',
description: nls.localize('schema.onEnterRules.beforeText.pattern', 'The RegExp pattern for beforeText.'),
default: '',
},
flags: {
type: 'string',
description: nls.localize('schema.onEnterRules.beforeText.flags', 'The RegExp flags for beforeText.'),
default: '',
pattern: '^([gimuy]+)$',
patternErrorMessage: nls.localize('schema.onEnterRules.beforeText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.')
}
}
},
afterText: {
type: ['string', 'object'],
description: nls.localize('schema.onEnterRules.afterText', 'This rule will only execute if the text after the cursor matches this regular expression.'),
properties: {
pattern: {
type: 'string',
description: nls.localize('schema.onEnterRules.afterText.pattern', 'The RegExp pattern for afterText.'),
default: '',
},
flags: {
type: 'string',
description: nls.localize('schema.onEnterRules.afterText.flags', 'The RegExp flags for afterText.'),
default: '',
pattern: '^([gimuy]+)$',
patternErrorMessage: nls.localize('schema.onEnterRules.afterText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.')
}
}
},
previousLineText: {
type: ['string', 'object'],
description: nls.localize('schema.onEnterRules.previousLineText', 'This rule will only execute if the text above the line matches this regular expression.'),
properties: {
pattern: {
type: 'string',
description: nls.localize('schema.onEnterRules.previousLineText.pattern', 'The RegExp pattern for previousLineText.'),
default: '',
},
flags: {
type: 'string',
description: nls.localize('schema.onEnterRules.previousLineText.flags', 'The RegExp flags for previousLineText.'),
default: '',
pattern: '^([gimuy]+)$',
patternErrorMessage: nls.localize('schema.onEnterRules.previousLineText.errorMessage', 'Must match the pattern `/^([gimuy]+)$/`.')
}
}
},
action: {
type: ['string', 'object'],
description: nls.localize('schema.onEnterRules.action', 'The action to execute.'),
required: ['indent'],
default: { 'indent': 'indent' },
properties: {
indent: {
type: 'string',
description: nls.localize('schema.onEnterRules.action.indent', "Describe what to do with the indentation"),
default: 'indent',
enum: ['none', 'indent', 'indentOutdent', 'outdent'],
markdownEnumDescriptions: [
nls.localize('schema.onEnterRules.action.indent.none', "Insert new line and copy the previous line's indentation."),
nls.localize('schema.onEnterRules.action.indent.indent', "Insert new line and indent once (relative to the previous line's indentation)."),
nls.localize('schema.onEnterRules.action.indent.indentOutdent', "Insert two new lines:\n - the first one indented which will hold the cursor\n - the second one at the same indentation level"),
nls.localize('schema.onEnterRules.action.indent.outdent', "Insert new line and outdent once (relative to the previous line's indentation).")
]
},
appendText: {
type: 'string',
description: nls.localize('schema.onEnterRules.action.appendText', 'Describes text to be appended after the new line and after the indentation.'),
default: '',
},
removeText: {
type: 'number',
description: nls.localize('schema.onEnterRules.action.removeText', 'Describes the number of characters to remove from the new line\'s indentation.'),
default: 0,
}
}
}
}
}
}
}
@@ -115,7 +115,8 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider<IPi
accept: async () => {
const pick = await provider.pick();
if (pick) {
await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: pick.config.type });
// Use the type of the provider, not of the config since config sometimes have subtypes (for example "node-terminal")
await configManager.selectConfiguration(pick.launch, pick.config.name, pick.config, { type: provider.type });
this.debugService.startDebugging(pick.launch, pick.config);
}
}
@@ -478,7 +478,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction {
|| !this.extension.local
|| this.extension.state !== ExtensionState.Installed
|| this.extension.type !== ExtensionType.User
|| this.extension.enablementState === EnablementState.DisabledByEnvironemt
|| this.extension.enablementState === EnablementState.DisabledByEnvironment
) {
return false;
}
@@ -67,7 +67,7 @@ export class ExternalUriOpenerService extends Disposable implements IExternalUri
return { dispose: remove };
}
async openExternal(href: string, ctx: { sourceUri: URI }, token: CancellationToken): Promise<boolean> {
async openExternal(href: string, ctx: { sourceUri: URI, preferredOpenerId?: string }, token: CancellationToken): Promise<boolean> {
const targetUri = typeof href === 'string' ? URI.parse(href) : href;
@@ -77,11 +77,24 @@ export class ExternalUriOpenerService extends Disposable implements IExternalUri
return false;
}
// First check to see if we have a configured opener
// First see if we have a preferredOpener
if (ctx.preferredOpenerId) {
if (ctx.preferredOpenerId === defaultExternalUriOpenerId) {
return false;
}
const preferredOpener = allOpeners.get(ctx.preferredOpenerId);
if (preferredOpener) {
// Skip the `canOpen` check here since the opener was specifically requested.
return preferredOpener.openExternalUri(targetUri, ctx, token);
}
}
// Check to see if we have a configured opener
const configuredOpener = this.getConfiguredOpenerForUri(allOpeners, targetUri);
if (configuredOpener) {
// Skip the `canOpen` check here since the opener was specifically requested.
return configuredOpener === 'default' ? false : configuredOpener.openExternalUri(targetUri, ctx, token);
return configuredOpener === defaultExternalUriOpenerId ? false : configuredOpener.openExternalUri(targetUri, ctx, token);
}
// Then check to see if there is a valid opener
@@ -232,7 +232,8 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri
this._register(this.editorService.overrideOpenEditor({
getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => {
const currentEditorForResource = group && this.editorService.findEditors(resource, group);
const currentEditorsForResource = group && this.editorService.findEditors(resource, group);
const currentEditorForResource = currentEditorsForResource && currentEditorsForResource.length ? currentEditorsForResource[0] : undefined;
const associatedEditors = distinct([
...this.getUserAssociatedNotebookEditors(resource),
@@ -336,7 +337,24 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri
}
if (originalInput instanceof NotebookEditorInput) {
return undefined;
if (originalInput.viewType === id) {
return undefined;
} else {
return {
override: (async () => {
const notebookInput = NotebookEditorInput.create(this.instantiationService, originalInput.resource, originalInput.getName(), id);
await group.replaceEditors([{
editor: originalInput,
replacement: notebookInput
}]);
if (group.activeEditorPane?.input === notebookInput) {
return group.activeEditorPane;
} else {
return undefined;
}
})()
};
}
}
if (originalInput instanceof NotebookDiffEditorInput) {
@@ -427,7 +427,7 @@ var requirejs = (function() {
if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto)
|| matchesScheme(link, Schemas.command)) {
this.openerService.open(link, { fromUserGesture: true });
this.openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true });
}
}));
@@ -253,7 +253,6 @@ export class OutlinePane extends ViewPane {
// update: refresh tree
this._domNode.classList.remove('message');
tree.updateChildren();
tree.expandAll();
}
};
updateTree();
@@ -3,44 +3,59 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITunnelService, TunnelOptions, RemoteTunnel, TunnelCreationOptions } from 'vs/platform/remote/common/tunnel';
import { ITunnelService, TunnelOptions, RemoteTunnel, TunnelCreationOptions, ITunnel } from 'vs/platform/remote/common/tunnel';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { URI } from 'vs/base/common/uri';
import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { ILogService } from 'vs/platform/log/common/log';
export class TunnelFactoryContribution extends Disposable implements IWorkbenchContribution {
constructor(
@ITunnelService tunnelService: ITunnelService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IOpenerService openerService: IOpenerService,
@IRemoteExplorerService remoteExplorerService: IRemoteExplorerService
@IRemoteExplorerService remoteExplorerService: IRemoteExplorerService,
@ILogService logService: ILogService
) {
super();
const tunnelFactory = environmentService.options?.tunnelProvider?.tunnelFactory;
if (tunnelFactory) {
this._register(tunnelService.setTunnelProvider({
forwardPort: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel> | undefined => {
const tunnelPromise = tunnelFactory(tunnelOptions, tunnelCreationOptions);
if (!tunnelPromise) {
return undefined;
forwardPort: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel | undefined> | undefined => {
let tunnelPromise: Promise<ITunnel> | undefined;
try {
tunnelPromise = tunnelFactory(tunnelOptions, tunnelCreationOptions);
} catch (e) {
logService.trace('tunnelFactory: tunnel provider error');
}
return new Promise(resolve => {
tunnelPromise.then(async (tunnel) => {
const localAddress = tunnel.localAddress.startsWith('http') ? tunnel.localAddress : `http://${tunnel.localAddress}`;
const remoteTunnel: RemoteTunnel = {
tunnelRemotePort: tunnel.remoteAddress.port,
tunnelRemoteHost: tunnel.remoteAddress.host,
// The tunnel factory may give us an inaccessible local address.
// To make sure this doesn't happen, resolve the uri immediately.
localAddress: (await openerService.resolveExternalUri(URI.parse(localAddress))).resolved.toString(),
public: !!tunnel.public,
dispose: async () => { await tunnel.dispose; }
};
resolve(remoteTunnel);
});
return new Promise(async (resolve) => {
if (!tunnelPromise) {
resolve(undefined);
return;
}
let tunnel: ITunnel;
try {
tunnel = await tunnelPromise;
} catch (e) {
logService.trace('tunnelFactory: tunnel provider promise error');
resolve(undefined);
return;
}
const localAddress = tunnel.localAddress.startsWith('http') ? tunnel.localAddress : `http://${tunnel.localAddress}`;
const remoteTunnel: RemoteTunnel = {
tunnelRemotePort: tunnel.remoteAddress.port,
tunnelRemoteHost: tunnel.remoteAddress.host,
// The tunnel factory may give us an inaccessible local address.
// To make sure this doesn't happen, resolve the uri immediately.
localAddress: (await openerService.resolveExternalUri(URI.parse(localAddress))).resolved.toString(),
public: !!tunnel.public,
dispose: async () => { await tunnel.dispose(); }
};
resolve(remoteTunnel);
});
}
}, environmentService.options?.tunnelProvider?.features ?? { elevation: false, public: false }));
@@ -239,7 +239,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
// Open HTTP(s) links with opener service
if (symbolToOpen.location.uri.scheme === Schemas.http || symbolToOpen.location.uri.scheme === Schemas.https) {
await this.openerService.open(symbolToOpen.location.uri, { fromUserGesture: true });
await this.openerService.open(symbolToOpen.location.uri, { fromUserGesture: true, allowContributedOpeners: true });
}
// Otherwise open as editor
@@ -187,7 +187,10 @@ export class TerminalLinkManager extends DisposableStore {
}
private _handleHypertextLink(url: string): void {
this._openerService.open(url, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) });
this._openerService.open(url, {
allowTunneling: !!(this._processManager && this._processManager.remoteAuthority),
allowContributedOpeners: true,
});
}
private async _handleProtocolLink(link: string): Promise<void> {
@@ -997,6 +997,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
case 'pwsh.exe':
return WindowsShellType.PowerShell;
case 'bash.exe':
case 'git-cmd.exe':
return WindowsShellType.GitBash;
case 'wsl.exe':
case 'ubuntu.exe':
@@ -15,7 +15,7 @@ import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/te
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
/**
* Projection that lists tests in their traditional tree view.
@@ -46,7 +46,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
this._register(listener.onFolderChange(this.applyFolderChange, this));
for (const [folder, collection] of listener.workspaceFolderCollections) {
const queue = [collection.rootNodes];
const queue = [collection.rootIds];
while (queue.length) {
for (const id of queue.pop()!) {
const node = collection.getNodeById(id)!;
@@ -10,7 +10,7 @@ import { HierarchicalByLocationProjection as HierarchicalByLocationProjection }
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
/**
* Type of test element in the list.
@@ -95,7 +95,7 @@ export class NodeChangeList<T extends ITestTreeElement & { children: Iterable<T>
pruneNodesNotInTree(this.changedParents, tree);
pruneNodesNotInTree(this.updatedNodes, tree);
const diffDeep = this.isFirstApply ? Infinity : 0;
const diffDepth = this.isFirstApply ? Infinity : 0;
this.isFirstApply = false;
for (let parent of this.changedParents) {
@@ -107,7 +107,7 @@ export class NodeChangeList<T extends ITestTreeElement & { children: Iterable<T>
tree.setChildren(
parent,
this.renderNodeList(renderNode, parent === null ? roots() : parent.children),
{ diffIdentityProvider: testIdentityProvider, diffDeep },
{ diffIdentityProvider: testIdentityProvider, diffDepth },
);
}
}
@@ -17,8 +17,8 @@ import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/
import { isRunningState, NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
import { statesInOrder } from 'vs/workbench/contrib/testing/browser/testExplorerTree';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
interface IStatusTestItem extends IncrementalTestCollectionItem {
treeElements: Map<TestRunState, TestStateElement>;
@@ -109,14 +109,7 @@ export class StateByLocationProjection extends AbstractIncrementalTestCollection
const firstDiff: TestsDiff = [];
for (const [, collection] of listener.workspaceFolderCollections) {
const queue = [collection.rootNodes];
while (queue.length) {
for (const id of queue.pop()!) {
const node = collection.getNodeById(id)!;
firstDiff.push([TestDiffOpType.Add, node]);
queue.push(node.children);
}
}
firstDiff.push(...collection.getReviverDiff());
}
this.apply(firstDiff);
@@ -17,8 +17,8 @@ import { ListElementType } from 'vs/workbench/contrib/testing/browser/explorerPr
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
import { isRunningState, NodeChangeList, NodeRenderFn } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
class ListTestStateElement implements ITestTreeElement {
public computedState = this.test.item.state.runState;
@@ -108,14 +108,7 @@ export class StateByNameProjection extends AbstractIncrementalTestCollection<ISt
const firstDiff: TestsDiff = [];
for (const [, collection] of listener.workspaceFolderCollections) {
const queue = [collection.rootNodes];
while (queue.length) {
for (const id of queue.pop()!) {
const node = collection.getNodeById(id)!;
firstDiff.push([TestDiffOpType.Add, node]);
queue.push(node.children);
}
}
firstDiff.push(...collection.getReviverDiff());
}
this.apply(firstDiff);
@@ -10,7 +10,7 @@ import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme';
export const testingViewIcon = registerIcon('testing-view-icon', Codicon.beaker, localize('testingViewIcon', 'View icon of the testing view.'));
export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.'));
export const testingRunIcon = registerIcon('testing-run-icon', Codicon.debugStart, localize('testingRunIcon', 'Icon of the "run test" action.'));
export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAlt, localize('testingDebugIcon', 'Icon of the "debug test" action.'));
export const testingCancelIcon = registerIcon('testing-cancel-icon', Codicon.close, localize('testingCancelIcon', 'Icon to cancel ongoing test runs.'));
@@ -5,38 +5,35 @@
import { Action } from 'vs/base/common/actions';
import { Emitter } from 'vs/base/common/event';
import { Codicon } from 'vs/base/common/codicons';
import { localize } from 'vs/nls';
import { Action2, MenuId } from 'vs/platform/actions/common/actions';
import { ContextKeyAndExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { ShowViewletAction2 } from 'vs/workbench/browser/viewlet';
import { CATEGORIES } from 'vs/workbench/common/actions';
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { ITestingCollectionService } from 'vs/workbench/contrib/testing/common/testingCollectionService';
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService';
import { INotificationService } from 'vs/platform/notification/common/notification';
export class FilterableAction extends Action {
private visChangeEmitter = new Emitter<boolean>();
const category = localize('testing.category', 'Test');
public onDidChangeVisibility = this.visChangeEmitter.event;
public isVisible = true;
protected _setVisible(isVisible: boolean) {
if (isVisible !== this.isVisible) {
this.isVisible = isVisible;
this.visChangeEmitter.fire(isVisible);
}
}
const enum ActionOrder {
Run = 10,
Debug,
Refresh,
Collapse,
}
export const filterVisibleActions = (actions: ReadonlyArray<Action>) =>
actions.filter(a => !(a instanceof FilterableAction) || a.isVisible);
export class DebugAction extends Action {
constructor(
private readonly tests: Iterable<TestIdWithProvider>,
@@ -44,7 +41,7 @@ export class DebugAction extends Action {
@ITestService private readonly testService: ITestService
) {
super(
'action.run',
'testing.run',
localize('debug test', 'Debug Test'),
'test-action ' + ThemeIcon.asClassName(icons.testingDebugIcon),
/* enabled= */ !isRunning
@@ -69,7 +66,7 @@ export class RunAction extends Action {
@ITestService private readonly testService: ITestService
) {
super(
'action.run',
'testing.run',
localize('run test', 'Run Test'),
'test-action ' + ThemeIcon.asClassName(icons.testingRunIcon),
/* enabled= */ !isRunning,
@@ -88,21 +85,14 @@ export class RunAction extends Action {
}
abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
constructor(id: string, title: string, icon: ThemeIcon) {
constructor(id: string, title: string, icon: ThemeIcon, private readonly debug: boolean) {
super({
id,
title,
icon,
viewId: Testing.ExplorerViewId,
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'navigation',
when: ContextKeyAndExpr.create([
ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId),
ContextKeyEqualsExpr.create(TestingContextKeys.isRunning.serialize(), false),
])
}
f1: true,
category,
});
}
@@ -110,15 +100,15 @@ abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
* @override
*/
public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise<RunTestsResult> {
const tests = this.getActionableTests(accessor.get(ITestingCollectionService), view.viewModel);
const tests = this.getActionableTests(accessor.get(IWorkspaceTestCollectionService), view.viewModel);
if (!tests.length) {
return Promise.resolve(EMPTY_TEST_RESULT);
}
return accessor.get(ITestService).runTests({ tests, debug: this.debug() });
return accessor.get(ITestService).runTests({ tests, debug: this.debug });
}
private getActionableTests(testCollection: ITestingCollectionService, viewModel: TestingExplorerViewModel) {
private getActionableTests(testCollection: IWorkspaceTestCollectionService, viewModel: TestingExplorerViewModel) {
const selected = viewModel.getSelectedTests();
const tests: TestIdWithProvider[] = [];
if (!selected.length) {
@@ -140,7 +130,6 @@ abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
return tests;
}
protected abstract debug(): boolean;
protected abstract filter(item: InternalTestItem): boolean;
}
@@ -148,19 +137,13 @@ export class RunSelectedAction extends RunOrDebugAction {
constructor(
) {
super(
'action.runSelected',
'testing.runSelected',
localize('runSelectedTests', 'Run Selected Tests'),
icons.testingRunIcon,
false,
);
}
/**
* @override
*/
public debug() {
return false;
}
/**
* @override
*/
@@ -172,19 +155,13 @@ export class RunSelectedAction extends RunOrDebugAction {
export class DebugSelectedAction extends RunOrDebugAction {
constructor() {
super(
'action.debugSelected',
'testing.debugSelected',
localize('debugSelectedTests', 'Debug Selected Tests'),
icons.testingDebugIcon,
true,
);
}
/**
* @override
*/
public debug() {
return true;
}
/**
* @override
*/
@@ -193,6 +170,81 @@ export class DebugSelectedAction extends RunOrDebugAction {
}
}
abstract class RunOrDebugAllAllAction extends Action2 {
constructor(id: string, title: string, icon: ThemeIcon, private readonly debug: boolean, private noTestsFoundError: string) {
super({
id,
title,
icon,
f1: true,
category,
menu: {
id: MenuId.ViewTitle,
order: debug ? ActionOrder.Debug : ActionOrder.Run,
group: 'navigation',
when: ContextKeyAndExpr.create([
ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId),
ContextKeyEqualsExpr.create(TestingContextKeys.isRunning.serialize(), false),
])
}
});
}
public async run(accessor: ServicesAccessor) {
const testService = accessor.get(ITestService);
const workspace = accessor.get(IWorkspaceContextService);
const notifications = accessor.get(INotificationService);
const tests: TestIdWithProvider[] = [];
await Promise.all(workspace.getWorkspace().folders.map(async (folder) => {
const handle = testService.subscribeToDiffs(ExtHostTestingResource.Workspace, folder.uri);
try {
await waitForAllRoots(handle.collection);
for (const root of handle.collection.rootIds) {
const node = handle.collection.getNodeById(root);
if (node && (this.debug ? node.item.debuggable : node.item.runnable)) {
tests.push({ testId: node.id, providerId: node.providerId });
}
}
} finally {
handle.dispose();
}
}));
if (tests.length === 0) {
notifications.info(this.noTestsFoundError);
return;
}
await testService.runTests({ tests, debug: this.debug });
}
}
export class RunAllAction extends RunOrDebugAllAllAction {
constructor() {
super(
'testing.runAll',
localize('runAllTests', 'Run All Tests'),
icons.testingDebugIcon,
false,
localize('noTestProvider', 'No tests found in this workspace. You may need to install a test provider extension'),
);
}
}
export class DebugAllAction extends RunOrDebugAllAllAction {
constructor() {
super(
'testing.debugAll',
localize('debugAllTests', 'Debug All Tests'),
icons.testingDebugIcon,
true,
localize('noDebugTestProvider', 'No debuggable tests found in this workspace. You may need to install a test provider extension'),
);
}
}
export class CancelTestRunAction extends Action2 {
constructor() {
super({
@@ -201,7 +253,7 @@ export class CancelTestRunAction extends Action2 {
icon: icons.testingCancelIcon,
menu: {
id: MenuId.ViewTitle,
order: 10,
order: ActionOrder.Run,
group: 'navigation',
when: ContextKeyAndExpr.create([
ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId),
@@ -323,14 +375,43 @@ export class TestingGroupByStatusAction extends ViewAction<TestingExplorerView>
}
}
export class CollapseAllAction extends ViewAction<TestingExplorerView> {
constructor() {
super({
id: 'testing.collapseAll',
viewId: Testing.ExplorerViewId,
title: localize('testing.collapseAll', "Collapse All Tests"),
f1: false,
icon: Codicon.collapseAll,
menu: {
id: MenuId.ViewTitle,
order: ActionOrder.Collapse,
group: 'navigation',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
}
/**
* @override
*/
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
view.viewModel.collapseAll();
}
}
export class RefreshTestsAction extends Action2 {
constructor() {
super({
id: 'testing.refreshTests',
title: localize('testing.refresh', "Refresh Tests"),
category,
f1: true,
icon: Codicon.refresh,
menu: {
id: MenuId.ViewTitle,
order: 0,
order: ActionOrder.Refresh,
group: 'navigation',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
@@ -343,3 +424,19 @@ export class RefreshTestsAction extends Action2 {
accessor.get(ITestService).resubscribeToAllTests();
}
}
export class ShowTestView extends ShowViewletAction2 {
constructor() {
super({
// matches old test action for back-compat
id: 'workbench.view.extension.test',
title: localize('showTestViewley', "Show Test"),
category: CATEGORIES.View.value,
f1: true,
});
}
protected viewletId() {
return Testing.ViewletId;
}
}
@@ -25,7 +25,7 @@ import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browse
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestingCollectionService, TestingCollectionService } from 'vs/workbench/contrib/testing/common/testingCollectionService';
import { IWorkspaceTestCollectionService, WorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
@@ -35,15 +35,15 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import * as Action from './testExplorerActions';
registerSingleton(ITestService, TestService);
registerSingleton(ITestingCollectionService, TestingCollectionService);
registerSingleton(IWorkspaceTestCollectionService, WorkspaceTestCollectionService);
const viewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({
id: Testing.ViewletId,
name: localize('testing', "Testing"),
name: localize('test', "Test"),
ctorDescriptor: new SyncDescriptor(TestingViewPaneContainer),
icon: testingViewIcon,
alwaysUseContainerInfo: true,
order: 5,
order: 6,
hideIfEmpty: true,
}, ViewContainerLocation.Sidebar);
@@ -87,6 +87,10 @@ registerAction2(Action.DebugSelectedAction);
registerAction2(Action.TestingGroupByLocationAction);
registerAction2(Action.TestingGroupByStatusAction);
registerAction2(Action.RefreshTestsAction);
registerAction2(Action.ShowTestView);
registerAction2(Action.CollapseAllAction);
registerAction2(Action.RunAllAction);
registerAction2(Action.DebugAllAction);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);
@@ -34,7 +34,6 @@ import { IProgressService } from 'vs/platform/progress/common/progress';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { IResourceLabel, IResourceLabelOptions, IResourceLabelProps, ResourceLabels } from 'vs/workbench/browser/labels';
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
@@ -52,7 +51,7 @@ import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/browser
import { TestingExplorerFilter, TestingFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { TestExplorerViewGrouping, TestExplorerViewMode } from 'vs/workbench/contrib/testing/common/constants';
import { ITestingCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/testingCollectionService';
import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -68,7 +67,7 @@ export class TestingExplorerView extends ViewPane {
constructor(
options: IViewletViewOptions,
@ITestingCollectionService private readonly testCollection: ITestingCollectionService,
@IWorkspaceTestCollectionService private readonly testCollection: IWorkspaceTestCollectionService,
@ITestService private readonly testService: ITestService,
@IProgressService private readonly progress: IProgressService,
@IContextMenuService contextMenuService: IContextMenuService,
@@ -106,13 +105,6 @@ export class TestingExplorerView extends ViewPane {
this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility, this.currentSubscription, this.filterState);
this._register(this.viewModel);
this.updateProgressIndicator();
this._register(this.testService.onBusyStateChange(t => {
if (t.resource === ExtHostTestingResource.Workspace && t.busy !== (!!this.finishDiscovery)) {
this.updateProgressIndicator();
}
}));
this.getProgressIndicator().show(true);
this._register(this.onDidChangeBodyVisibility(visible => {
@@ -135,8 +127,7 @@ export class TestingExplorerView extends ViewPane {
this.filter.saveState();
}
private updateProgressIndicator() {
const busy = Iterable.some(this.testService.busyTestLocations, s => s.resource === ExtHostTestingResource.Workspace);
private updateProgressIndicator(busy: number) {
if (!busy && this.finishDiscovery) {
this.finishDiscovery();
this.finishDiscovery = undefined;
@@ -156,7 +147,9 @@ export class TestingExplorerView extends ViewPane {
}
private createSubscription() {
return this.testCollection.subscribeToWorkspaceTests();
const handle = this.testCollection.subscribeToWorkspaceTests();
handle.subscription.onBusyProvidersChange(() => this.updateProgressIndicator(handle.subscription.busyProviders));
return handle;
}
}
@@ -309,6 +302,13 @@ export class TestingExplorerViewModel extends Disposable {
this.tree.setSelection([item]);
}
/**
* Collapse all items in the tree.
*/
public async collapseAll() {
this.tree.collapseAll();
}
/**
* Opens an editor for the item. If there is a failure associated with the
* test item, it will be shown.
@@ -7,7 +7,8 @@ import { localize } from 'vs/nls';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
export const enum Testing {
ViewletId = 'workbench.view.testing',
// marked as "extension" so that any existing test extensions are assigned to it.
ViewletId = 'workbench.view.extension.test',
ExplorerViewId = 'workbench.view.testing',
OutputPeekContributionId = 'editor.contrib.testingOutputPeek',
}
@@ -9,7 +9,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { generateUuid } from 'vs/base/common/uuid';
import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters';
import { RequiredTestItem, TestItem as ApiTestItem } from 'vs/workbench/api/common/extHostTypes';
import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem, TestDiffOpType, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection';
/**
* @private
@@ -103,6 +103,14 @@ export class SingleUseTestCollection implements IDisposable {
return diff;
}
/**
* Pushes a new diff entry onto the collected diff list.
*/
public pushDiff(diff: TestsDiffOp) {
this.diff.push(diff);
this.debounceSendDiff.schedule();
}
public dispose() {
for (const item of this.testItemToInternal.values()) {
this.testIdToInternal.delete(item.id);
@@ -82,15 +82,23 @@ export interface InternalTestItem {
}
export const enum TestDiffOpType {
/** Adds a new test (with children) */
Add,
/** Shallow-updates an existing test */
Update,
/** Removes a test (and all its children) */
Remove,
/** Changes the number of providers running initial test discovery. */
DeltaDiscoverComplete,
/** Changes the number of providers who are yet to publish their collection roots. */
DeltaRootsComplete,
}
export type TestsDiffOp =
| [op: TestDiffOpType.Add, item: InternalTestItem]
| [op: TestDiffOpType.Update, item: InternalTestItem]
| [op: TestDiffOpType.Remove, itemId: string];
| [op: TestDiffOpType.Remove, itemId: string]
| [op: TestDiffOpType.DeltaDiscoverComplete | TestDiffOpType.DeltaRootsComplete, amount: number];
/**
* Utility function to get a unique string for a subscription to a resource,
@@ -213,13 +221,38 @@ export abstract class AbstractIncrementalTestCollection<T extends IncrementalTes
}
}
}
break;
}
case TestDiffOpType.DeltaDiscoverComplete:
this.updateBusyProviders(op[1]);
break;
case TestDiffOpType.DeltaRootsComplete:
this.updatePendingRoots(op[1]);
break;
}
}
changes.complete();
}
/**
* Updates the number of providers who are still discovering items.
*/
protected updateBusyProviders(delta: number) {
// no-op
}
/**
* Updates the number of test root sources who are yet to report. When
* the total pending test roots reaches 0, the roots for all providers
* will exist in the collection.
*/
protected updatePendingRoots(delta: number) {
// no-op
}
/**
* Called before a diff is applied to create a new change collector.
*/

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