diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 11da75e482d..81e08f8e0a6 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -53,6 +53,7 @@ const buildfile = require('../src/buildfile'); const vscodeWebEntryPoints = [ buildfile.workbenchWeb, buildfile.serviceWorker, + buildfile.workerExtensionHost, buildfile.keyboardMaps, buildfile.base ]; @@ -148,4 +149,4 @@ const dashed = (str) => (str ? `-${str}` : ``); vscodeWebTaskCI )); gulp.task(vscodeWebTask); -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index 88e2c32174b..20211acd200 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.38.0", - "distro": "6ce1c040c0d555b998dba62dd333f437a7b2d44f", + "distro": "afe23e028d98eeca8e26c62804b876ad9cce27b9", "author": { "name": "Microsoft Corporation" }, @@ -101,7 +101,7 @@ "gulp-shell": "^0.6.5", "gulp-tsb": "2.0.7", "gulp-tslint": "^8.1.3", - "gulp-uglify": "^3.0.0", + "gulp-uglify": "^3.0.2", "gulp-untar": "^0.0.7", "gulp-vinyl-zip": "^2.1.2", "http-server": "^0.11.1", @@ -134,7 +134,7 @@ "tslint": "^5.16.0", "typescript": "3.5.2", "typescript-formatter": "7.1.0", - "uglify-es": "^3.0.18", + "uglify-es": "^3.3.9", "underscore": "^1.8.2", "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", diff --git a/src/buildfile.js b/src/buildfile.js index f9946efc703..43fcfa807c3 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -function entrypoint (name) { +function entrypoint(name) { return [{ name: name, include: [], exclude: ['vs/css', 'vs/nls'] }]; } @@ -23,6 +23,14 @@ exports.serviceWorker = [{ dest: 'vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.js' }]; +exports.workerExtensionHost = [{ + name: 'vs/workbench/services/extensions/worker/extensionHostWorker', + // include: [], + prepend: ['vs/loader.js'], + append: ['vs/workbench/services/extensions/worker/extensionHostWorkerMain'], + dest: 'vs/workbench/services/extensions/worker/extensionHostWorkerMain.js' +}]; + exports.workbench = require('./vs/workbench/buildfile').collectModules(['vs/workbench/workbench.desktop.main']); exports.workbenchWeb = entrypoint('vs/workbench/workbench.web.api'); diff --git a/src/vs/base/browser/formattedTextRenderer.ts b/src/vs/base/browser/formattedTextRenderer.ts new file mode 100644 index 00000000000..0e9b0c9571c --- /dev/null +++ b/src/vs/base/browser/formattedTextRenderer.ts @@ -0,0 +1,220 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +export interface IContentActionHandler { + callback: (content: string, event?: IMouseEvent) => void; + readonly disposeables: DisposableStore; +} + +export interface FormattedTextRenderOptions { + readonly className?: string; + readonly inline?: boolean; + readonly actionHandler?: IContentActionHandler; +} + +export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement { + const element = createElement(options); + element.textContent = text; + return element; +} + +export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement { + const element = createElement(options); + _renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler); + return element; +} + +export function createElement(options: FormattedTextRenderOptions): HTMLElement { + const tagName = options.inline ? 'span' : 'div'; + const element = document.createElement(tagName); + if (options.className) { + element.className = options.className; + } + return element; +} + +class StringStream { + private source: string; + private index: number; + + constructor(source: string) { + this.source = source; + this.index = 0; + } + + public eos(): boolean { + return this.index >= this.source.length; + } + + public next(): string { + const next = this.peek(); + this.advance(); + return next; + } + + public peek(): string { + return this.source[this.index]; + } + + public advance(): void { + this.index++; + } +} + +const enum FormatType { + Invalid, + Root, + Text, + Bold, + Italics, + Action, + ActionClose, + NewLine +} + +interface IFormatParseTree { + type: FormatType; + content?: string; + index?: number; + children?: IFormatParseTree[]; +} + +function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) { + let child: Node | undefined; + + if (treeNode.type === FormatType.Text) { + child = document.createTextNode(treeNode.content || ''); + } else if (treeNode.type === FormatType.Bold) { + child = document.createElement('b'); + } else if (treeNode.type === FormatType.Italics) { + child = document.createElement('i'); + } else if (treeNode.type === FormatType.Action && actionHandler) { + const a = document.createElement('a'); + a.href = '#'; + actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => { + actionHandler.callback(String(treeNode.index), event); + })); + + child = a; + } else if (treeNode.type === FormatType.NewLine) { + child = document.createElement('br'); + } else if (treeNode.type === FormatType.Root) { + child = element; + } + + if (child && element !== child) { + element.appendChild(child); + } + + if (child && Array.isArray(treeNode.children)) { + treeNode.children.forEach((nodeChild) => { + _renderFormattedText(child!, nodeChild, actionHandler); + }); + } +} + +function parseFormattedText(content: string): IFormatParseTree { + + const root: IFormatParseTree = { + type: FormatType.Root, + children: [] + }; + + let actionViewItemIndex = 0; + let current = root; + const stack: IFormatParseTree[] = []; + const stream = new StringStream(content); + + while (!stream.eos()) { + let next = stream.next(); + + const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid); + if (isEscapedFormatType) { + next = stream.next(); // unread the backslash if it escapes a format tag type + } + + if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) { + stream.advance(); + + if (current.type === FormatType.Text) { + current = stack.pop()!; + } + + const type = formatTagType(next); + if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) { + current = stack.pop()!; + } else { + const newCurrent: IFormatParseTree = { + type: type, + children: [] + }; + + if (type === FormatType.Action) { + newCurrent.index = actionViewItemIndex; + actionViewItemIndex++; + } + + current.children!.push(newCurrent); + stack.push(current); + current = newCurrent; + } + } else if (next === '\n') { + if (current.type === FormatType.Text) { + current = stack.pop()!; + } + + current.children!.push({ + type: FormatType.NewLine + }); + + } else { + if (current.type !== FormatType.Text) { + const textCurrent: IFormatParseTree = { + type: FormatType.Text, + content: next + }; + current.children!.push(textCurrent); + stack.push(current); + current = textCurrent; + + } else { + current.content += next; + } + } + } + + if (current.type === FormatType.Text) { + current = stack.pop()!; + } + + if (stack.length) { + // incorrectly formatted string literal + } + + return root; +} + +function isFormatTag(char: string): boolean { + return formatTagType(char) !== FormatType.Invalid; +} + +function formatTagType(char: string): FormatType { + switch (char) { + case '*': + return FormatType.Bold; + case '_': + return FormatType.Italics; + case '[': + return FormatType.Action; + case ']': + return FormatType.ActionClose; + default: + return FormatType.Invalid; + } +} diff --git a/src/vs/base/browser/htmlContentRenderer.ts b/src/vs/base/browser/htmlContentRenderer.ts deleted file mode 100644 index 4506912e178..00000000000 --- a/src/vs/base/browser/htmlContentRenderer.ts +++ /dev/null @@ -1,394 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as DOM from 'vs/base/browser/dom'; -import { defaultGenerator } from 'vs/base/common/idGenerator'; -import { escape } from 'vs/base/common/strings'; -import { removeMarkdownEscapes, IMarkdownString, parseHrefAndDimensions } from 'vs/base/common/htmlContent'; -import * as marked from 'vs/base/common/marked/marked'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { URI } from 'vs/base/common/uri'; -import { parse } from 'vs/base/common/marshalling'; -import { cloneAndChange } from 'vs/base/common/objects'; - -export interface IContentActionHandler { - callback: (content: string, event?: IMouseEvent) => void; - readonly disposeables: DisposableStore; -} - -export interface RenderOptions { - className?: string; - inline?: boolean; - actionHandler?: IContentActionHandler; - codeBlockRenderer?: (modeId: string, value: string) => Promise; - codeBlockRenderCallback?: () => void; -} - -function createElement(options: RenderOptions): HTMLElement { - const tagName = options.inline ? 'span' : 'div'; - const element = document.createElement(tagName); - if (options.className) { - element.className = options.className; - } - return element; -} - -export function renderText(text: string, options: RenderOptions = {}): HTMLElement { - const element = createElement(options); - element.textContent = text; - return element; -} - -export function renderFormattedText(formattedText: string, options: RenderOptions = {}): HTMLElement { - const element = createElement(options); - _renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler); - return element; -} - -/** - * Create html nodes for the given content element. - */ -export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions = {}): HTMLElement { - const element = createElement(options); - - const _uriMassage = function (part: string): string { - let data: any; - try { - data = parse(decodeURIComponent(part)); - } catch (e) { - // ignore - } - if (!data) { - return part; - } - data = cloneAndChange(data, value => { - if (markdown.uris && markdown.uris[value]) { - return URI.revive(markdown.uris[value]); - } else { - return undefined; - } - }); - return encodeURIComponent(JSON.stringify(data)); - }; - - const _href = function (href: string, isDomUri: boolean): string { - const data = markdown.uris && markdown.uris[href]; - if (!data) { - return href; - } - let uri = URI.revive(data); - if (isDomUri) { - uri = DOM.asDomUri(uri); - } - if (uri.query) { - uri = uri.with({ query: _uriMassage(uri.query) }); - } - if (data) { - href = uri.toString(true); - } - return href; - }; - - // signal to code-block render that the - // element has been created - let signalInnerHTML: () => void; - const withInnerHTML = new Promise(c => signalInnerHTML = c); - - const renderer = new marked.Renderer(); - renderer.image = (href: string, title: string, text: string) => { - let dimensions: string[] = []; - let attributes: string[] = []; - if (href) { - ({ href, dimensions } = parseHrefAndDimensions(href)); - href = _href(href, true); - attributes.push(`src="${href}"`); - } - if (text) { - attributes.push(`alt="${text}"`); - } - if (title) { - attributes.push(`title="${title}"`); - } - if (dimensions.length) { - attributes = attributes.concat(dimensions); - } - return ''; - }; - renderer.link = (href, title, text): string => { - // Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829 - if (href === text) { // raw link case - text = removeMarkdownEscapes(text); - } - href = _href(href, false); - title = removeMarkdownEscapes(title); - href = removeMarkdownEscapes(href); - if ( - !href - || href.match(/^data:|javascript:/i) - || (href.match(/^command:/i) && !markdown.isTrusted) - || href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i) - ) { - // drop the link - return text; - - } else { - // HTML Encode href - href = href.replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - return `${text}`; - } - }; - renderer.paragraph = (text): string => { - return `

${text}

`; - }; - - if (options.codeBlockRenderer) { - renderer.code = (code, lang) => { - const value = options.codeBlockRenderer!(lang, code); - // when code-block rendering is async we return sync - // but update the node with the real result later. - const id = defaultGenerator.nextId(); - const promise = Promise.all([value, withInnerHTML]).then(values => { - const strValue = values[0]; - const span = element.querySelector(`div[data-code="${id}"]`); - if (span) { - span.innerHTML = strValue; - } - }).catch(err => { - // ignore - }); - - if (options.codeBlockRenderCallback) { - promise.then(options.codeBlockRenderCallback); - } - - return `
${escape(code)}
`; - }; - } - - if (options.actionHandler) { - options.actionHandler.disposeables.add(DOM.addStandardDisposableListener(element, 'click', event => { - let target: HTMLElement | null = event.target; - if (target.tagName !== 'A') { - target = target.parentElement; - if (!target || target.tagName !== 'A') { - return; - } - } - try { - const href = target.dataset['href']; - if (href) { - options.actionHandler!.callback(href, event); - } - } catch (err) { - onUnexpectedError(err); - } finally { - event.preventDefault(); - } - })); - } - - const markedOptions: marked.MarkedOptions = { - sanitize: true, - renderer - }; - - element.innerHTML = marked.parse(markdown.value, markedOptions); - signalInnerHTML!(); - - return element; -} - -// --- formatted string parsing - -class StringStream { - private source: string; - private index: number; - - constructor(source: string) { - this.source = source; - this.index = 0; - } - - public eos(): boolean { - return this.index >= this.source.length; - } - - public next(): string { - const next = this.peek(); - this.advance(); - return next; - } - - public peek(): string { - return this.source[this.index]; - } - - public advance(): void { - this.index++; - } -} - -const enum FormatType { - Invalid, - Root, - Text, - Bold, - Italics, - Action, - ActionClose, - NewLine -} - -interface IFormatParseTree { - type: FormatType; - content?: string; - index?: number; - children?: IFormatParseTree[]; -} - -function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) { - let child: Node | undefined; - - if (treeNode.type === FormatType.Text) { - child = document.createTextNode(treeNode.content || ''); - } - else if (treeNode.type === FormatType.Bold) { - child = document.createElement('b'); - } - else if (treeNode.type === FormatType.Italics) { - child = document.createElement('i'); - } - else if (treeNode.type === FormatType.Action && actionHandler) { - const a = document.createElement('a'); - a.href = '#'; - actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => { - actionHandler.callback(String(treeNode.index), event); - })); - - child = a; - } - else if (treeNode.type === FormatType.NewLine) { - child = document.createElement('br'); - } - else if (treeNode.type === FormatType.Root) { - child = element; - } - - if (child && element !== child) { - element.appendChild(child); - } - - if (child && Array.isArray(treeNode.children)) { - treeNode.children.forEach((nodeChild) => { - _renderFormattedText(child!, nodeChild, actionHandler); - }); - } -} - -function parseFormattedText(content: string): IFormatParseTree { - - const root: IFormatParseTree = { - type: FormatType.Root, - children: [] - }; - - let actionViewItemIndex = 0; - let current = root; - const stack: IFormatParseTree[] = []; - const stream = new StringStream(content); - - while (!stream.eos()) { - let next = stream.next(); - - const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid); - if (isEscapedFormatType) { - next = stream.next(); // unread the backslash if it escapes a format tag type - } - - if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) { - stream.advance(); - - if (current.type === FormatType.Text) { - current = stack.pop()!; - } - - const type = formatTagType(next); - if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) { - current = stack.pop()!; - } else { - const newCurrent: IFormatParseTree = { - type: type, - children: [] - }; - - if (type === FormatType.Action) { - newCurrent.index = actionViewItemIndex; - actionViewItemIndex++; - } - - current.children!.push(newCurrent); - stack.push(current); - current = newCurrent; - } - } else if (next === '\n') { - if (current.type === FormatType.Text) { - current = stack.pop()!; - } - - current.children!.push({ - type: FormatType.NewLine - }); - - } else { - if (current.type !== FormatType.Text) { - const textCurrent: IFormatParseTree = { - type: FormatType.Text, - content: next - }; - current.children!.push(textCurrent); - stack.push(current); - current = textCurrent; - - } else { - current.content += next; - } - } - } - - if (current.type === FormatType.Text) { - current = stack.pop()!; - } - - if (stack.length) { - // incorrectly formatted string literal - } - - return root; -} - -function isFormatTag(char: string): boolean { - return formatTagType(char) !== FormatType.Invalid; -} - -function formatTagType(char: string): FormatType { - switch (char) { - case '*': - return FormatType.Bold; - case '_': - return FormatType.Italics; - case '[': - return FormatType.Action; - case ']': - return FormatType.ActionClose; - default: - return FormatType.Invalid; - } -} diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts new file mode 100644 index 00000000000..7fd9f63b75a --- /dev/null +++ b/src/vs/base/browser/markdownRenderer.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent'; +import { defaultGenerator } from 'vs/base/common/idGenerator'; +import * as marked from 'vs/base/common/marked/marked'; +import { parse } from 'vs/base/common/marshalling'; +import { cloneAndChange } from 'vs/base/common/objects'; +import { escape } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; + +export interface MarkdownRenderOptions extends FormattedTextRenderOptions { + codeBlockRenderer?: (modeId: string, value: string) => Promise; + codeBlockRenderCallback?: () => void; +} + +/** + * Create html nodes for the given content element. + */ +export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}): HTMLElement { + const element = createElement(options); + + const _uriMassage = function (part: string): string { + let data: any; + try { + data = parse(decodeURIComponent(part)); + } catch (e) { + // ignore + } + if (!data) { + return part; + } + data = cloneAndChange(data, value => { + if (markdown.uris && markdown.uris[value]) { + return URI.revive(markdown.uris[value]); + } else { + return undefined; + } + }); + return encodeURIComponent(JSON.stringify(data)); + }; + + const _href = function (href: string, isDomUri: boolean): string { + const data = markdown.uris && markdown.uris[href]; + if (!data) { + return href; + } + let uri = URI.revive(data); + if (isDomUri) { + uri = DOM.asDomUri(uri); + } + if (uri.query) { + uri = uri.with({ query: _uriMassage(uri.query) }); + } + if (data) { + href = uri.toString(true); + } + return href; + }; + + // signal to code-block render that the + // element has been created + let signalInnerHTML: () => void; + const withInnerHTML = new Promise(c => signalInnerHTML = c); + + const renderer = new marked.Renderer(); + renderer.image = (href: string, title: string, text: string) => { + let dimensions: string[] = []; + let attributes: string[] = []; + if (href) { + ({ href, dimensions } = parseHrefAndDimensions(href)); + href = _href(href, true); + attributes.push(`src="${href}"`); + } + if (text) { + attributes.push(`alt="${text}"`); + } + if (title) { + attributes.push(`title="${title}"`); + } + if (dimensions.length) { + attributes = attributes.concat(dimensions); + } + return ''; + }; + renderer.link = (href, title, text): string => { + // Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829 + if (href === text) { // raw link case + text = removeMarkdownEscapes(text); + } + href = _href(href, false); + title = removeMarkdownEscapes(title); + href = removeMarkdownEscapes(href); + if ( + !href + || href.match(/^data:|javascript:/i) + || (href.match(/^command:/i) && !markdown.isTrusted) + || href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i) + ) { + // drop the link + return text; + + } else { + // HTML Encode href + href = href.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + return `${text}`; + } + }; + renderer.paragraph = (text): string => { + return `

${text}

`; + }; + + if (options.codeBlockRenderer) { + renderer.code = (code, lang) => { + const value = options.codeBlockRenderer!(lang, code); + // when code-block rendering is async we return sync + // but update the node with the real result later. + const id = defaultGenerator.nextId(); + const promise = Promise.all([value, withInnerHTML]).then(values => { + const strValue = values[0]; + const span = element.querySelector(`div[data-code="${id}"]`); + if (span) { + span.innerHTML = strValue; + } + }).catch(err => { + // ignore + }); + + if (options.codeBlockRenderCallback) { + promise.then(options.codeBlockRenderCallback); + } + + return `
${escape(code)}
`; + }; + } + + if (options.actionHandler) { + options.actionHandler.disposeables.add(DOM.addStandardDisposableListener(element, 'click', event => { + let target: HTMLElement | null = event.target; + if (target.tagName !== 'A') { + target = target.parentElement; + if (!target || target.tagName !== 'A') { + return; + } + } + try { + const href = target.dataset['href']; + if (href) { + options.actionHandler!.callback(href, event); + } + } catch (err) { + onUnexpectedError(err); + } finally { + event.preventDefault(); + } + })); + } + + const markedOptions: marked.MarkedOptions = { + sanitize: true, + renderer + }; + + element.innerHTML = marked.parse(markdown.value, markedOptions); + signalInnerHTML!(); + + return element; +} diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 59d0c9900b9..7af2a58f46d 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -8,7 +8,8 @@ import 'vs/css!./inputBox'; import * as nls from 'vs/nls'; import * as Bal from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; -import { RenderOptions, renderFormattedText, renderText } from 'vs/base/browser/htmlContentRenderer'; +import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; +import { renderFormattedText, renderText } from 'vs/base/browser/formattedTextRenderer'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { IAction } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -438,7 +439,7 @@ export class InputBox extends Widget { div = dom.append(container, $('.monaco-inputbox-container')); layout(); - const renderOptions: RenderOptions = { + const renderOptions: MarkdownRenderOptions = { inline: true, className: 'monaco-inputbox-message' }; diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 134c64bbef4..d144eb63050 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -18,7 +18,7 @@ import { domEvent } from 'vs/base/browser/event'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ISelectBoxDelegate, ISelectOptionItem, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox'; import { isMacintosh } from 'vs/base/common/platform'; -import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; const $ = dom.$; diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index d4397d3279d..71185d07046 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -729,3 +729,18 @@ export function getNLines(str: string, n = 1): string { str.substr(0, idx) : str; } + +/** + * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc. + */ +export function singleLetterHash(n: number): string { + const LETTERS_CNT = (CharCode.Z - CharCode.A + 1); + + n = n % (2 * LETTERS_CNT); + + if (n < LETTERS_CNT) { + return String.fromCharCode(CharCode.a + n); + } + + return String.fromCharCode(CharCode.A + n - LETTERS_CNT); +} diff --git a/src/vs/base/test/browser/htmlContent.test.ts b/src/vs/base/test/browser/formattedTextRenderer.test.ts similarity index 65% rename from src/vs/base/test/browser/htmlContent.test.ts rename to src/vs/base/test/browser/formattedTextRenderer.test.ts index d5dec0729d9..0be97478522 100644 --- a/src/vs/base/test/browser/htmlContent.test.ts +++ b/src/vs/base/test/browser/formattedTextRenderer.test.ts @@ -2,12 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; -import * as marked from 'vs/base/common/marked/marked'; -import { renderMarkdown, renderText, renderFormattedText } from 'vs/base/browser/htmlContentRenderer'; +import { renderText, renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { DisposableStore } from 'vs/base/common/lifecycle'; -suite('HtmlContent', () => { +suite('FormattedTextRenderer', () => { const store = new DisposableStore(); setup(() => { @@ -101,36 +101,4 @@ suite('HtmlContent', () => { assert.strictEqual(result.children.length, 0); assert.strictEqual(result.innerHTML, '**bold**'); }); - test('image rendering conforms to default', () => { - const markdown = { value: `![image](someimageurl 'caption')` }; - const result: HTMLElement = renderMarkdown(markdown); - const renderer = new marked.Renderer(); - const imageFromMarked = marked(markdown.value, { - sanitize: true, - renderer - }).trim(); - assert.strictEqual(result.innerHTML, imageFromMarked); - }); - test('image rendering conforms to default without title', () => { - const markdown = { value: `![image](someimageurl)` }; - const result: HTMLElement = renderMarkdown(markdown); - const renderer = new marked.Renderer(); - const imageFromMarked = marked(markdown.value, { - sanitize: true, - renderer - }).trim(); - assert.strictEqual(result.innerHTML, imageFromMarked); - }); - test('image width from title params', () => { - let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` }); - assert.strictEqual(result.innerHTML, `

image

`); - }); - test('image height from title params', () => { - let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` }); - assert.strictEqual(result.innerHTML, `

image

`); - }); - test('image width and height from title params', () => { - let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` }); - assert.strictEqual(result.innerHTML, `

image

`); - }); }); diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts new file mode 100644 index 00000000000..525eb86e741 --- /dev/null +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as marked from 'vs/base/common/marked/marked'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; + +suite('MarkdownRenderer', () => { + test('image rendering conforms to default', () => { + const markdown = { value: `![image](someimageurl 'caption')` }; + const result: HTMLElement = renderMarkdown(markdown); + const renderer = new marked.Renderer(); + const imageFromMarked = marked(markdown.value, { + sanitize: true, + renderer + }).trim(); + assert.strictEqual(result.innerHTML, imageFromMarked); + }); + + test('image rendering conforms to default without title', () => { + const markdown = { value: `![image](someimageurl)` }; + const result: HTMLElement = renderMarkdown(markdown); + const renderer = new marked.Renderer(); + const imageFromMarked = marked(markdown.value, { + sanitize: true, + renderer + }).trim(); + assert.strictEqual(result.innerHTML, imageFromMarked); + }); + + test('image width from title params', () => { + let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` }); + assert.strictEqual(result.innerHTML, `

image

`); + }); + + test('image height from title params', () => { + let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` }); + assert.strictEqual(result.innerHTML, `

image

`); + }); + + test('image width and height from title params', () => { + let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` }); + assert.strictEqual(result.innerHTML, `

image

`); + }); +}); diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index a201e8b84ef..d8dbad25721 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -9,6 +9,7 @@ import { TestView, nodesToArrays } from './util'; import { deepClone } from 'vs/base/common/objects'; // Simple example: +// // +-----+---------------+ // | 4 | 2 | // +-----+---------+-----+ @@ -16,6 +17,16 @@ import { deepClone } from 'vs/base/common/objects'; // +---------------+ 3 | // | 5 | | // +---------------+-----+ +// +// V +// +-H +// | +-4 +// | +-2 +// +-H +// | +-V +// | +-1 +// | +-5 +// +-3 suite('Grid', function () { let container: HTMLElement; diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index c2a7ddc54e6..bce60b6b855 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -19,26 +19,31 @@ function getWorker(workerId: string, label: string): Worker | Promise { // ESM-comment-begin if (typeof require === 'function') { // check if the JS lives on a different origin - const workerMain = require.toUrl('./' + workerId); - if (/^(http:)|(https:)|(file:)/.test(workerMain)) { - const currentUrl = String(window.location); - const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length); - if (workerMain.substring(0, currentOrigin.length) !== currentOrigin) { - // this is the cross-origin case - // i.e. the webpage is running at a different origin than where the scripts are loaded from - const workerBaseUrl = workerMain.substr(0, workerMain.length - 'vs/base/worker/workerMain.js'.length); - const js = `/*${label}*/self.MonacoEnvironment={baseUrl: '${workerBaseUrl}'};importScripts('${workerMain}');/*${label}*/`; - const url = `data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`; - return new Worker(url); - } - } - return new Worker(workerMain + '#' + label); + const workerUrl = getWorkerBootstrapUrl(workerMain, label); + return new Worker(workerUrl, { name: label }); } // ESM-comment-end throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`); } +export function getWorkerBootstrapUrl(scriptPath: string, label: string): string { + if (/^(http:)|(https:)|(file:)/.test(scriptPath)) { + const currentUrl = String(window.location); + const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length); + if (scriptPath.substring(0, currentOrigin.length) !== currentOrigin) { + // this is the cross-origin case + // i.e. the webpage is running at a different origin than where the scripts are loaded from + const myPath = 'vs/base/worker/defaultWorkerFactory.js'; + const workerBaseUrl = require.toUrl(myPath).slice(0, -myPath.length); + const js = `/*${label}*/self.MonacoEnvironment={baseUrl: '${workerBaseUrl}'};importScripts('${scriptPath}');/*${label}*/`; + const url = `data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`; + return url; + } + } + return scriptPath + '#' + label; +} + function isPromiseLike(obj: any): obj is PromiseLike { if (typeof obj.then === 'function') { return true; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index d71ddf83ea1..15e9c438ddb 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -7,7 +7,7 @@ import { app, ipcMain as ipc, systemPreferences, shell, Event, contentTracing, p import { IProcessEnvironment, isWindows, isMacintosh } from 'vs/base/common/platform'; import { WindowsManager } from 'vs/code/electron-main/windows'; import { IWindowsService, OpenContext, ActiveWindowManager, IURIToOpen } from 'vs/platform/windows/common/windows'; -import { WindowsChannel } from 'vs/platform/windows/node/windowsIpc'; +import { WindowsChannel } from 'vs/platform/windows/common/windowsIpc'; import { WindowsService } from 'vs/platform/windows/electron-main/windowsService'; import { ILifecycleService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { getShellEnvironment } from 'vs/code/node/shellEnv'; diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 06d4b2f5307..418e8ebee98 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -49,7 +49,7 @@ export interface IPointerHandlerHelper { */ getLastViewCursorsRenderData(): IViewCursorRenderData[]; - shouldSuppressMouseDownOnViewZone(viewZoneId: number): boolean; + shouldSuppressMouseDownOnViewZone(viewZoneId: string): boolean; shouldSuppressMouseDownOnWidget(widgetId: string): boolean; /** diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 1f4c6ff52c7..f510e0f6f82 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -19,7 +19,7 @@ import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; export interface IViewZoneData { - viewZoneId: number; + viewZoneId: string; positionBefore: Position | null; positionAfter: Position | null; position: Position; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 496e9cae7ab..91e6ea3864b 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -83,17 +83,17 @@ export interface IViewZoneChangeAccessor { * @param zone Zone to create * @return A unique identifier to the view zone. */ - addZone(zone: IViewZone): number; + addZone(zone: IViewZone): string; /** * Remove a zone * @param id A unique identifier to the view zone, as returned by the `addZone` call. */ - removeZone(id: number): void; + removeZone(id: string): void; /** * Change a zone's position. * The editor will rescan the `afterLineNumber` and `afterColumn` properties of a view zone. */ - layoutZone(id: number): void; + layoutZone(id: string): void; } /** @@ -399,7 +399,7 @@ export interface ICodeEditor extends editorCommon.IEditor { */ onWillType(listener: (text: string) => void): IDisposable; /** - * An event emitted before interpreting typed characters (on the keyboard). + * An event emitted after interpreting typed characters (on the keyboard). * @event * @internal */ diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index c175034f969..608d6e3b5d9 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -55,8 +55,7 @@ export class OpenerService implements IOpenerService { if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https) || equalsIgnoreCase(scheme, Schemas.mailto)) { // open http or default mail application - dom.windowOpenNoOpener(encodeURI(resource.toString(true))); - return Promise.resolve(true); + return this.openExternal(resource); } else if (equalsIgnoreCase(scheme, Schemas.command)) { // run command or bail out if command isn't known @@ -100,4 +99,10 @@ export class OpenerService implements IOpenerService { ).then(() => true); } } + + openExternal(resource: URI): Promise { + dom.windowOpenNoOpener(encodeURI(resource.toString(true))); + + return Promise.resolve(true); + } } diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index a476c33912a..45c711cecef 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -248,7 +248,7 @@ export class View extends ViewEventHandler { getLastViewCursorsRenderData: () => { return this.viewCursors.getLastRenderData() || []; }, - shouldSuppressMouseDownOnViewZone: (viewZoneId: number) => { + shouldSuppressMouseDownOnViewZone: (viewZoneId: string) => { return this.viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId); }, shouldSuppressMouseDownOnWidget: (widgetId: string) => { @@ -473,17 +473,17 @@ export class View extends ViewEventHandler { this._renderOnce(() => { const changeAccessor: editorBrowser.IViewZoneChangeAccessor = { - addZone: (zone: editorBrowser.IViewZone): number => { + addZone: (zone: editorBrowser.IViewZone): string => { zonesHaveChanged = true; return this.viewZones.addZone(zone); }, - removeZone: (id: number): void => { + removeZone: (id: string): void => { if (!id) { return; } zonesHaveChanged = this.viewZones.removeZone(id) || zonesHaveChanged; }, - layoutZone: (id: number): void => { + layoutZone: (id: string): void => { if (!id) { return; } diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index a51fbe5bf02..6c01df4e0b6 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -14,7 +14,7 @@ import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewWhitespaceViewportData } from 'vs/editor/common/viewModel/viewModel'; export interface IMyViewZone { - whitespaceId: number; + whitespaceId: string; delegate: IViewZone; isVisible: boolean; domNode: FastDomNode; @@ -74,7 +74,7 @@ export class ViewZones extends ViewPart { const id = keys[i]; const zone = this._zones[id]; const props = this._computeWhitespaceProps(zone.delegate); - if (this._context.viewLayout.changeWhitespace(parseInt(id, 10), props.afterViewLineNumber, props.heightInPx)) { + if (this._context.viewLayout.changeWhitespace(id, props.afterViewLineNumber, props.heightInPx)) { this._safeCallOnComputedHeight(zone.delegate, props.heightInPx); hadAChange = true; } @@ -183,7 +183,7 @@ export class ViewZones extends ViewPart { }; } - public addZone(zone: IViewZone): number { + public addZone(zone: IViewZone): string { const props = this._computeWhitespaceProps(zone); const whitespaceId = this._context.viewLayout.addWhitespace(props.afterViewLineNumber, this._getZoneOrdinal(zone), props.heightInPx, props.minWidthInPx); @@ -200,18 +200,18 @@ export class ViewZones extends ViewPart { myZone.domNode.setPosition('absolute'); myZone.domNode.domNode.style.width = '100%'; myZone.domNode.setDisplay('none'); - myZone.domNode.setAttribute('monaco-view-zone', myZone.whitespaceId.toString()); + myZone.domNode.setAttribute('monaco-view-zone', myZone.whitespaceId); this.domNode.appendChild(myZone.domNode); if (myZone.marginDomNode) { myZone.marginDomNode.setPosition('absolute'); myZone.marginDomNode.domNode.style.width = '100%'; myZone.marginDomNode.setDisplay('none'); - myZone.marginDomNode.setAttribute('monaco-view-zone', myZone.whitespaceId.toString()); + myZone.marginDomNode.setAttribute('monaco-view-zone', myZone.whitespaceId); this.marginDomNode.appendChild(myZone.marginDomNode); } - this._zones[myZone.whitespaceId.toString()] = myZone; + this._zones[myZone.whitespaceId] = myZone; this.setShouldRender(); @@ -219,10 +219,10 @@ export class ViewZones extends ViewPart { return myZone.whitespaceId; } - public removeZone(id: number): boolean { - if (this._zones.hasOwnProperty(id.toString())) { - const zone = this._zones[id.toString()]; - delete this._zones[id.toString()]; + public removeZone(id: string): boolean { + if (this._zones.hasOwnProperty(id)) { + const zone = this._zones[id]; + delete this._zones[id]; this._context.viewLayout.removeWhitespace(zone.whitespaceId); zone.domNode.removeAttribute('monaco-visible-view-zone'); @@ -242,10 +242,10 @@ export class ViewZones extends ViewPart { return false; } - public layoutZone(id: number): boolean { + public layoutZone(id: string): boolean { let changed = false; - if (this._zones.hasOwnProperty(id.toString())) { - const zone = this._zones[id.toString()]; + if (this._zones.hasOwnProperty(id)) { + const zone = this._zones[id]; const props = this._computeWhitespaceProps(zone.delegate); // const newOrdinal = this._getZoneOrdinal(zone.delegate); changed = this._context.viewLayout.changeWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx) || changed; @@ -259,9 +259,9 @@ export class ViewZones extends ViewPart { return changed; } - public shouldSuppressMouseDownOnViewZone(id: number): boolean { - if (this._zones.hasOwnProperty(id.toString())) { - const zone = this._zones[id.toString()]; + public shouldSuppressMouseDownOnViewZone(id: string): boolean { + if (this._zones.hasOwnProperty(id)) { + const zone = this._zones[id]; return Boolean(zone.delegate.suppressMouseDown); } return false; @@ -314,7 +314,7 @@ export class ViewZones extends ViewPart { let hasVisibleZone = false; for (let i = 0, len = visibleWhitespaces.length; i < len; i++) { - visibleZones[visibleWhitespaces[i].id.toString()] = visibleWhitespaces[i]; + visibleZones[visibleWhitespaces[i].id] = visibleWhitespaces[i]; hasVisibleZone = true; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 260f479df55..ee32431b77a 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -69,7 +69,7 @@ interface IDiffEditorWidgetStyle { } class VisualEditorState { - private _zones: number[]; + private _zones: string[]; private _zonesMap: { [zoneId: string]: boolean; }; private _decorations: string[]; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 874d7428cf3..0469471f8c4 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -110,21 +110,6 @@ export function createTextBuffer(value: string | model.ITextBufferFactory, defau let MODEL_ID = 0; -/** - * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc. - */ -function singleLetter(result: number): string { - const LETTERS_CNT = (CharCode.Z - CharCode.A + 1); - - result = result % (2 * LETTERS_CNT); - - if (result < LETTERS_CNT) { - return String.fromCharCode(CharCode.a + result); - } - - return String.fromCharCode(CharCode.A + result - LETTERS_CNT); -} - const LIMIT_FIND_COUNT = 999; export const LONG_LINE_BOUNDARY = 10000; @@ -343,7 +328,7 @@ export class TextModel extends Disposable implements model.ITextModel { } }); - this._instanceId = singleLetter(MODEL_ID); + this._instanceId = strings.singleLetterHash(MODEL_ID); this._lastDecorationId = 0; this._decorations = Object.create(null); this._decorationsTree = new DecorationsTrees(); diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index 2967fb3b003..d6673ac8b8f 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -63,14 +63,14 @@ export class LinesLayout { * @param heightInPx The height of the whitespace, in pixels. * @return An id that can be used later to mutate or delete the whitespace */ - public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): number { + public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string { return this._whitespaces.insertWhitespace(afterLineNumber, ordinal, heightInPx, minWidth); } /** * Change properties associated with a certain whitespace. */ - public changeWhitespace(id: number, newAfterLineNumber: number, newHeight: number): boolean { + public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { return this._whitespaces.changeWhitespace(id, newAfterLineNumber, newHeight); } @@ -80,7 +80,7 @@ export class LinesLayout { * @param id The whitespace to remove * @return Returns true if the whitespace is found and it is removed. */ - public removeWhitespace(id: number): boolean { + public removeWhitespace(id: string): boolean { return this._whitespaces.removeWhitespace(id); } diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index f753f65d393..ad24479b4b2 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -173,13 +173,13 @@ export class ViewLayout extends Disposable implements IViewLayout { // ---- IVerticalLayoutProvider - public addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): number { + public addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): string { return this._linesLayout.insertWhitespace(afterLineNumber, ordinal, height, minWidth); } - public changeWhitespace(id: number, newAfterLineNumber: number, newHeight: number): boolean { + public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { return this._linesLayout.changeWhitespace(id, newAfterLineNumber, newHeight); } - public removeWhitespace(id: number): boolean { + public removeWhitespace(id: string): boolean { return this._linesLayout.removeWhitespace(id); } public getVerticalOffsetForLineNumber(lineNumber: number): number { diff --git a/src/vs/editor/common/viewLayout/whitespaceComputer.ts b/src/vs/editor/common/viewLayout/whitespaceComputer.ts index 537c36ac807..8e5dd347e83 100644 --- a/src/vs/editor/common/viewLayout/whitespaceComputer.ts +++ b/src/vs/editor/common/viewLayout/whitespaceComputer.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as strings from 'vs/base/common/strings'; + export interface IEditorWhitespace { - readonly id: number; + readonly id: string; readonly afterLineNumber: number; readonly heightInLines: number; } @@ -15,6 +17,10 @@ export interface IEditorWhitespace { */ export class WhitespaceComputer { + private static INSTANCE_COUNT = 0; + + private readonly _instanceId: string; + /** * heights[i] is the height in pixels for whitespace at index i */ @@ -48,7 +54,7 @@ export class WhitespaceComputer { /** * ids[i] is the whitespace id of whitespace at index i */ - private readonly _ids: number[]; + private readonly _ids: string[]; /** * index at which a whitespace is positioned (inside heights, afterLineNumbers, prefixSum members) @@ -65,6 +71,7 @@ export class WhitespaceComputer { private _minWidth: number; constructor() { + this._instanceId = strings.singleLetterHash(++WhitespaceComputer.INSTANCE_COUNT); this._heights = []; this._minWidths = []; this._ids = []; @@ -113,21 +120,20 @@ export class WhitespaceComputer { * @param heightInPx The height of the whitespace, in pixels. * @return An id that can be used later to mutate or delete the whitespace */ - public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): number { + public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string { afterLineNumber = afterLineNumber | 0; ordinal = ordinal | 0; heightInPx = heightInPx | 0; minWidth = minWidth | 0; - let id = (++this._lastWhitespaceId); + let id = this._instanceId + (++this._lastWhitespaceId); let insertionIndex = WhitespaceComputer.findInsertionIndex(this._afterLineNumbers, afterLineNumber, this._ordinals, ordinal); this._insertWhitespaceAtIndex(id, insertionIndex, afterLineNumber, ordinal, heightInPx, minWidth); this._minWidth = -1; /* marker for not being computed */ return id; } - private _insertWhitespaceAtIndex(id: number, insertIndex: number, afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): void { - id = id | 0; + private _insertWhitespaceAtIndex(id: string, insertIndex: number, afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): void { insertIndex = insertIndex | 0; afterLineNumber = afterLineNumber | 0; ordinal = ordinal | 0; @@ -150,15 +156,14 @@ export class WhitespaceComputer { } } - this._whitespaceId2Index[id.toString()] = insertIndex; + this._whitespaceId2Index[id] = insertIndex; this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, insertIndex - 1); } /** * Change properties associated with a certain whitespace. */ - public changeWhitespace(id: number, newAfterLineNumber: number, newHeight: number): boolean { - id = id | 0; + public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { newAfterLineNumber = newAfterLineNumber | 0; newHeight = newHeight | 0; @@ -175,13 +180,11 @@ export class WhitespaceComputer { * @param newHeightInPx The new height of the whitespace, in pixels * @return Returns true if the whitespace is found and if the new height is different than the old height */ - public changeWhitespaceHeight(id: number, newHeightInPx: number): boolean { - id = id | 0; + public changeWhitespaceHeight(id: string, newHeightInPx: number): boolean { newHeightInPx = newHeightInPx | 0; - let sid = id.toString(); - if (this._whitespaceId2Index.hasOwnProperty(sid)) { - let index = this._whitespaceId2Index[sid]; + if (this._whitespaceId2Index.hasOwnProperty(id)) { + let index = this._whitespaceId2Index[id]; if (this._heights[index] !== newHeightInPx) { this._heights[index] = newHeightInPx; this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, index - 1); @@ -198,13 +201,11 @@ export class WhitespaceComputer { * @param newAfterLineNumber The new line number the whitespace will follow * @return Returns true if the whitespace is found and if the new line number is different than the old line number */ - public changeWhitespaceAfterLineNumber(id: number, newAfterLineNumber: number): boolean { - id = id | 0; + public changeWhitespaceAfterLineNumber(id: string, newAfterLineNumber: number): boolean { newAfterLineNumber = newAfterLineNumber | 0; - let sid = id.toString(); - if (this._whitespaceId2Index.hasOwnProperty(sid)) { - let index = this._whitespaceId2Index[sid]; + if (this._whitespaceId2Index.hasOwnProperty(id)) { + let index = this._whitespaceId2Index[id]; if (this._afterLineNumbers[index] !== newAfterLineNumber) { // `afterLineNumber` changed for this whitespace @@ -236,14 +237,10 @@ export class WhitespaceComputer { * @param id The whitespace to remove * @return Returns true if the whitespace is found and it is removed. */ - public removeWhitespace(id: number): boolean { - id = id | 0; - - let sid = id.toString(); - - if (this._whitespaceId2Index.hasOwnProperty(sid)) { - let index = this._whitespaceId2Index[sid]; - delete this._whitespaceId2Index[sid]; + public removeWhitespace(id: string): boolean { + if (this._whitespaceId2Index.hasOwnProperty(id)) { + let index = this._whitespaceId2Index[id]; + delete this._whitespaceId2Index[id]; this._removeWhitespaceAtIndex(index); this._minWidth = -1; /* marker for not being computed */ return true; @@ -459,7 +456,7 @@ export class WhitespaceComputer { * @param index The index of the whitespace. * @return `id` of whitespace at `index`. */ - public getIdForWhitespaceIndex(index: number): number { + public getIdForWhitespaceIndex(index: number): string { index = index | 0; return this._ids[index]; diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index fe832ef3707..0f16d0a84a2 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -17,7 +17,7 @@ import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceCompute import { ITheme } from 'vs/platform/theme/common/themeService'; export interface IViewWhitespaceViewportData { - readonly id: number; + readonly id: string; readonly afterLineNumber: number; readonly verticalOffset: number; readonly height: number; @@ -74,15 +74,15 @@ export interface IViewLayout { * Reserve rendering space. * @return an identifier that can be later used to remove or change the whitespace. */ - addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): number; + addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): string; /** * Change the properties of a whitespace. */ - changeWhitespace(id: number, newAfterLineNumber: number, newHeight: number): boolean; + changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean; /** * Remove rendering space */ - removeWhitespace(id: number): boolean; + removeWhitespace(id: string): boolean; /** * Get the layout information for whitespaces currently in the viewport */ diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index cb41dc9f459..5ae20b27edb 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -193,7 +193,7 @@ export class CodeLensWidget { private readonly _editor: editorBrowser.ICodeEditor; private readonly _viewZone!: CodeLensViewZone; - private readonly _viewZoneId!: number; + private readonly _viewZoneId!: string; private readonly _contentWidget!: CodeLensContentWidget; private _decorationIds: string[]; private _data: CodeLensItem[]; diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 529d582ee47..a25d3b5b464 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -116,7 +116,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private readonly _replaceFocusTracker: dom.IFocusTracker; private readonly _replaceInputFocused: IContextKey; private _viewZone?: FindWidgetViewZone; - private _viewZoneId?: number; + private _viewZoneId?: string; private _resizeSash!: Sash; private _resized!: boolean; @@ -224,15 +224,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas if (!this._isVisible) { return; } - if (this._viewZoneId === undefined) { - return; - } - this._codeEditor.changeViewZones((accessor) => { - if (this._viewZoneId) { - accessor.removeZone(this._viewZoneId); - } - this._viewZoneId = undefined; - }); + this._viewZoneId = undefined; })); diff --git a/src/vs/editor/contrib/markdown/markdownRenderer.ts b/src/vs/editor/contrib/markdown/markdownRenderer.ts index 5825a919bf5..81f0b2ccc87 100644 --- a/src/vs/editor/contrib/markdown/markdownRenderer.ts +++ b/src/vs/editor/contrib/markdown/markdownRenderer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { renderMarkdown, RenderOptions } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown, MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; import { IModeService } from 'vs/editor/common/services/modeService'; import { URI } from 'vs/base/common/uri'; @@ -33,7 +33,7 @@ export class MarkdownRenderer extends Disposable { super(); } - private getOptions(disposeables: DisposableStore): RenderOptions { + private getOptions(disposeables: DisposableStore): MarkdownRenderOptions { return { codeBlockRenderer: (languageAlias, value) => { // In markdown, diff --git a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts index 2e7073d55a6..3e0fe0a7d66 100644 --- a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts @@ -51,7 +51,7 @@ const WIDGET_ID = 'vs.editor.contrib.zoneWidget'; export class ViewZoneDelegate implements IViewZone { public domNode: HTMLElement; - public id: number = 0; // A valid zone id should be greater than 0 + public id: string = ''; // A valid zone id should be greater than 0 public afterLineNumber: number; public afterColumn: number; public heightInLines: number; diff --git a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts index 22e860f8eb0..1981153c0c8 100644 --- a/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts +++ b/src/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.ts @@ -7,7 +7,7 @@ import 'vs/css!./accessibilityHelp'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { renderFormattedText } from 'vs/base/browser/htmlContentRenderer'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Widget } from 'vs/base/browser/ui/widget'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a797b72a6f2..b44b3ae08ad 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3605,17 +3605,17 @@ declare namespace monaco.editor { * @param zone Zone to create * @return A unique identifier to the view zone. */ - addZone(zone: IViewZone): number; + addZone(zone: IViewZone): string; /** * Remove a zone * @param id A unique identifier to the view zone, as returned by the `addZone` call. */ - removeZone(id: number): void; + removeZone(id: string): void; /** * Change a zone's position. * The editor will rescan the `afterLineNumber` and `afterColumn` properties of a view zone. */ - layoutZone(id: number): void; + layoutZone(id: string): void; } /** diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index e291d436115..42fbff47fe2 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -342,10 +342,10 @@ export class ExtensionGalleryService implements IExtensionGalleryService { @IProductService private readonly productService: IProductService, @optional(IStorageService) private readonly storageService: IStorageService, ) { - const config = productService.extensionsGallery; + const config = productService.productConfiguration.extensionsGallery; this.extensionsGalleryUrl = config && config.serviceUrl; this.extensionsControlUrl = config && config.controlUrl; - this.commonHeadersPromise = resolveMarketplaceHeaders(productService.version, this.environmentService, this.fileService, this.storageService); + this.commonHeadersPromise = resolveMarketplaceHeaders(productService.productConfiguration.version, this.environmentService, this.fileService, this.storageService); } private api(path = ''): string { @@ -358,7 +358,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise { const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1; - if (extension && extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) { + if (extension && extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.productConfiguration.version)) { return Promise.resolve(extension); } const { id, uuid } = extension ? extension.identifier : arg1; @@ -384,7 +384,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const versionAsset = rawExtension.versions.filter(v => v.version === version)[0]; if (versionAsset) { const extension = toExtension(rawExtension, versionAsset, 0, query); - if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) { + if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.productConfiguration.version)) { return extension; } } @@ -619,7 +619,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return this.queryGallery(query, CancellationToken.None).then(({ galleryExtensions }) => { if (galleryExtensions.length) { if (compatible) { - return Promise.all(galleryExtensions[0].versions.map(v => this.getEngine(v).then(engine => isEngineValid(engine, this.productService.version) ? v : null))) + return Promise.all(galleryExtensions[0].versions.map(v => this.getEngine(v).then(engine => isEngineValid(engine, this.productService.productConfiguration.version) ? v : null))) .then(versions => versions .filter(v => !!v) .map(v => ({ version: v!.version, date: v!.lastUpdated }))); @@ -705,7 +705,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (!engine) { return null; } - if (isEngineValid(engine, this.productService.version)) { + if (isEngineValid(engine, this.productService.productConfiguration.version)) { return Promise.resolve(version); } } @@ -737,7 +737,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const version = versions[0]; return this.getEngine(version) .then(engine => { - if (!isEngineValid(engine, this.productService.version)) { + if (!isEngineValid(engine, this.productService.productConfiguration.version)) { return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1)); } @@ -817,4 +817,4 @@ export async function resolveMarketplaceHeaders(version: string, environmentServ return headers; -} \ No newline at end of file +} diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 93a52746e6b..a043fdfcaa3 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -106,7 +106,7 @@ export interface IExtensionContributions { localizations?: ILocalization[]; } -export type ExtensionKind = 'ui' | 'workspace'; +export type ExtensionKind = 'ui' | 'workspace' | 'web'; export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { return thing @@ -221,4 +221,4 @@ export interface IExtensionDescription extends IExtensionManifest { export function isLanguagePackExtension(manifest: IExtensionManifest): boolean { return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false; -} \ No newline at end of file +} diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 8c0f9fee5df..c8336fc712c 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -9,7 +9,6 @@ import { IDisposable } from 'vs/base/common/lifecycle'; export const IOpenerService = createDecorator('openerService'); - export interface IOpener { open(resource: URI, options?: { openToSide?: boolean }): Promise; } @@ -18,6 +17,9 @@ export interface IOpenerService { _serviceBrand: any; + /** + * Register a participant that can handle the open() call. + */ registerOpener(opener: IOpener): IDisposable; /** @@ -27,10 +29,18 @@ export interface IOpenerService { * @return A promise that resolves when the opening is done. */ open(resource: URI, options?: { openToSide?: boolean }): Promise; + + /** + * Opens a URL externally. + * + * @param url A resource to open externally. + */ + openExternal(resource: URI): Promise; } export const NullOpenerService: IOpenerService = Object.freeze({ _serviceBrand: undefined, registerOpener() { return { dispose() { } }; }, - open() { return Promise.resolve(false); } + open() { return Promise.resolve(false); }, + openExternal() { return Promise.resolve(false); } }); diff --git a/src/vs/platform/product/browser/productService.ts b/src/vs/platform/product/browser/productService.ts index 70bc0b31bcc..fae6e1d5fa7 100644 --- a/src/vs/platform/product/browser/productService.ts +++ b/src/vs/platform/product/browser/productService.ts @@ -10,40 +10,17 @@ export class ProductService implements IProductService { _serviceBrand!: ServiceIdentifier; - private readonly productConfiguration: IProductConfiguration | null; + readonly productConfiguration: IProductConfiguration; constructor() { const element = document.getElementById('vscode-remote-product-configuration'); - this.productConfiguration = element ? JSON.parse(element.getAttribute('data-settings')!) : null; + this.productConfiguration = { + ...element ? JSON.parse(element.getAttribute('data-settings')!) : { + version: '1.38.0-unknown', + nameLong: 'Unknown', + extensionAllowedProposedApi: [], + }, ...{ urlProtocol: '', enableTelemetry: false } + }; } - get version(): string { return this.productConfiguration && this.productConfiguration.version ? this.productConfiguration.version : '1.38.0-unknown'; } - - get commit(): string | undefined { return this.productConfiguration ? this.productConfiguration.commit : undefined; } - - get nameLong(): string { return this.productConfiguration ? this.productConfiguration.nameLong : 'Unknown'; } - - get urlProtocol(): string { return ''; } - - get extensionAllowedProposedApi(): readonly string[] { return this.productConfiguration ? this.productConfiguration.extensionAllowedProposedApi : []; } - - get uiExtensions(): readonly string[] | undefined { return this.productConfiguration ? this.productConfiguration.uiExtensions : undefined; } - - get enableTelemetry(): boolean { return false; } - - get sendASmile(): { reportIssueUrl: string, requestFeatureUrl: string } | undefined { return this.productConfiguration ? this.productConfiguration.sendASmile : undefined; } - - get extensionsGallery() { return this.productConfiguration ? this.productConfiguration.extensionsGallery : undefined; } - - get settingsSearchBuildId(): number | undefined { return this.productConfiguration ? this.productConfiguration.settingsSearchBuildId : undefined; } - - get settingsSearchUrl(): string | undefined { return this.productConfiguration ? this.productConfiguration.settingsSearchUrl : undefined; } - - get experimentsUrl(): string | undefined { return this.productConfiguration ? this.productConfiguration.experimentsUrl : undefined; } - - get extensionKeywords(): { [extension: string]: readonly string[]; } | undefined { return this.productConfiguration ? this.productConfiguration.extensionKeywords : undefined; } - - get extensionAllowedBadgeProviders(): readonly string[] | undefined { return this.productConfiguration ? this.productConfiguration.extensionAllowedBadgeProviders : undefined; } - - get aiConfig() { return this.productConfiguration ? this.productConfiguration.aiConfig : undefined; } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 05048ee0908..11b6fed267a 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -11,38 +11,7 @@ export interface IProductService { _serviceBrand: ServiceIdentifier; - readonly version: string; - readonly commit?: string; - readonly date?: string; - - readonly nameLong: string; - readonly urlProtocol: string; - readonly extensionAllowedProposedApi: readonly string[]; - readonly uiExtensions?: readonly string[]; - - readonly enableTelemetry: boolean; - readonly extensionsGallery?: { - readonly serviceUrl: string; - readonly itemUrl: string; - readonly controlUrl: string; - readonly recommendationsUrl: string; - }; - - readonly sendASmile?: { - readonly reportIssueUrl: string; - readonly requestFeatureUrl: string; - }; - - readonly settingsSearchBuildId?: number; - readonly settingsSearchUrl?: string; - - readonly experimentsUrl?: string; - readonly extensionKeywords?: { [extension: string]: readonly string[]; }; - readonly extensionAllowedBadgeProviders?: readonly string[]; - - readonly aiConfig?: { - readonly asimovKey: string; - }; + readonly productConfiguration: IProductConfiguration; } export interface IProductConfiguration { diff --git a/src/vs/platform/product/node/productService.ts b/src/vs/platform/product/node/productService.ts index ba9c84a551d..f3986577355 100644 --- a/src/vs/platform/product/node/productService.ts +++ b/src/vs/platform/product/node/productService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IProductService } from 'vs/platform/product/common/product'; +import { IProductService, IProductConfiguration } from 'vs/platform/product/common/product'; import product from 'vs/platform/product/node/product'; import pkg from 'vs/platform/product/node/package'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -12,31 +12,12 @@ export class ProductService implements IProductService { _serviceBrand!: ServiceIdentifier; - get version(): string { return pkg.version; } + readonly productConfiguration: IProductConfiguration; - get commit(): string | undefined { return product.commit; } + constructor() { + this.productConfiguration = { + ...product, ...{ version: pkg.version } + }; + } - get nameLong(): string { return product.nameLong; } - - get urlProtocol(): string { return product.urlProtocol; } - - get extensionAllowedProposedApi(): readonly string[] { return product.extensionAllowedProposedApi; } - - get uiExtensions(): readonly string[] | undefined { return product.uiExtensions; } - - get enableTelemetry(): boolean { return product.enableTelemetry; } - - get sendASmile(): { reportIssueUrl: string, requestFeatureUrl: string } { return product.sendASmile; } - - get extensionsGallery() { return product.extensionsGallery; } - - get settingsSearchBuildId(): number | undefined { return product.settingsSearchBuildId; } - - get settingsSearchUrl(): string | undefined { return product.settingsSearchUrl; } - - get experimentsUrl(): string | undefined { return product.experimentsUrl; } - - get extensionKeywords(): { [extension: string]: readonly string[]; } | undefined { return product.extensionKeywords; } - - get extensionAllowedBadgeProviders(): readonly string[] | undefined { return product.extensionAllowedBadgeProviders; } } diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 392ca58f2b8..a11505e32ac 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -144,8 +144,10 @@ export class BrowserStorageService extends Disposable implements IStorageService // Signal as event so that clients can still store data this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); - // Close DBs - this.globalStorage.close(); - this.workspaceStorage.close(); + // We explicitly do not close our DBs because writing data onBeforeUnload() + // can result in unexpected results. Namely, it seems that - even though this + // operation is async - sometimes it is being triggered on unload and + // succeeds. Often though, the DBs turn out to be empty because the write + // never had a chance to complete. } } diff --git a/src/vs/platform/telemetry/browser/workbenchCommonProperties.ts b/src/vs/platform/telemetry/browser/workbenchCommonProperties.ts index 582ace32809..8f2d2413061 100644 --- a/src/vs/platform/telemetry/browser/workbenchCommonProperties.ts +++ b/src/vs/platform/telemetry/browser/workbenchCommonProperties.ts @@ -12,6 +12,7 @@ export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; import * as Platform from 'vs/base/common/platform'; import * as uuid from 'vs/base/common/uuid'; +import { cleanRemoteAuthority } from 'vs/platform/telemetry/common/telemetryUtils'; export async function resolveWorkbenchCommonProperties(storageService: IStorageService, commit: string | undefined, version: string | undefined, machineId: string, remoteAuthority?: string): Promise<{ [name: string]: string | undefined }> { const result: { [name: string]: string | undefined; } = Object.create(null); @@ -69,18 +70,3 @@ export async function resolveWorkbenchCommonProperties(storageService: IStorageS return result; } -function cleanRemoteAuthority(remoteAuthority?: string): string { - if (!remoteAuthority) { - return 'none'; - } - - let ret = 'other'; - // Whitelisted remote authorities - ['ssh-remote', 'dev-container', 'attached-container', 'wsl'].forEach((res: string) => { - if (remoteAuthority!.indexOf(`${res}+`) === 0) { - ret = res; - } - }); - - return ret; -} diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index f8990d64a32..d98abe9e156 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -288,6 +288,22 @@ export function validateTelemetryData(data?: any): { properties: Properties, mea }; } +export function cleanRemoteAuthority(remoteAuthority?: string): string { + if (!remoteAuthority) { + return 'none'; + } + + let ret = 'other'; + // Whitelisted remote authorities + ['ssh-remote', 'dev-container', 'attached-container', 'wsl'].forEach((res: string) => { + if (remoteAuthority!.indexOf(`${res}+`) === 0) { + ret = res; + } + }); + + return ret; +} + function flatten(obj: any, result: { [key: string]: any }, order: number = 0, prefix?: string): void { if (!obj) { return; diff --git a/src/vs/platform/telemetry/node/workbenchCommonProperties.ts b/src/vs/platform/telemetry/node/workbenchCommonProperties.ts index d1b83a14146..81d6a4d3d22 100644 --- a/src/vs/platform/telemetry/node/workbenchCommonProperties.ts +++ b/src/vs/platform/telemetry/node/workbenchCommonProperties.ts @@ -6,6 +6,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { cleanRemoteAuthority } from 'vs/platform/telemetry/common/telemetryUtils'; export async function resolveWorkbenchCommonProperties(storageService: IStorageService, commit: string | undefined, version: string | undefined, machineId: string, installSourcePath: string, remoteAuthority?: string): Promise<{ [name: string]: string | undefined }> { const result = await resolveCommonProperties(commit, version, machineId, installSourcePath); @@ -30,19 +31,3 @@ export async function resolveWorkbenchCommonProperties(storageService: IStorageS return result; } - -function cleanRemoteAuthority(remoteAuthority?: string): string { - if (!remoteAuthority) { - return 'none'; - } - - let ret = 'other'; - // Whitelisted remote authorities - ['ssh-remote', 'dev-container', 'wsl'].forEach((res: string) => { - if (remoteAuthority!.indexOf(`${res}+`) === 0) { - ret = res; - } - }); - - return ret; -} diff --git a/src/vs/platform/windows/node/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts similarity index 100% rename from src/vs/platform/windows/node/windowsIpc.ts rename to src/vs/platform/windows/common/windowsIpc.ts diff --git a/src/vs/platform/windows/electron-browser/windowsService.ts b/src/vs/platform/windows/electron-browser/windowsService.ts index 228dfd98ba2..57bd96143af 100644 --- a/src/vs/platform/windows/electron-browser/windowsService.ts +++ b/src/vs/platform/windows/electron-browser/windowsService.ts @@ -13,17 +13,14 @@ import { URI } from 'vs/base/common/uri'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { IProcessEnvironment } from 'vs/base/common/platform'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; export class WindowsService implements IWindowsService { - _serviceBrand: any; + _serviceBrand!: ServiceIdentifier; private channel: IChannel; - constructor(@IMainProcessService mainProcessService: IMainProcessService) { - this.channel = mainProcessService.getChannel('windows'); - } - get onWindowOpen(): Event { return this.channel.listen('onWindowOpen'); } get onWindowFocus(): Event { return this.channel.listen('onWindowFocus'); } get onWindowBlur(): Event { return this.channel.listen('onWindowBlur'); } @@ -31,6 +28,10 @@ export class WindowsService implements IWindowsService { get onWindowUnmaximize(): Event { return this.channel.listen('onWindowUnmaximize'); } get onRecentlyOpenedChange(): Event { return this.channel.listen('onRecentlyOpenedChange'); } + constructor(@IMainProcessService mainProcessService: IMainProcessService) { + this.channel = mainProcessService.getChannel('windows'); + } + pickFileFolderAndOpen(options: INativeOpenDialogOptions): Promise { return this.channel.call('pickFileFolderAndOpen', options); } diff --git a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts index 1e23e14ce69..2e9ea44a233 100644 --- a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts +++ b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts @@ -21,7 +21,7 @@ class EditorWebviewZone implements IViewZone { readonly afterColumn: number; readonly heightInLines: number; - private _id?: number; + private _id?: string; // suppressMouseDown?: boolean | undefined; // heightInPx?: number | undefined; // minWidthInPx?: number | undefined; diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 90bdeb5ea74..f2c3141d063 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -327,7 +327,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) { return true; } - if (this._productService.urlProtocol === link.scheme) { + if (this._productService.productConfiguration.urlProtocol === link.scheme) { return true; } return !!webview.webview.contentOptions.enableCommandUris && link.scheme === 'command'; diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 410bb4ec66d..acbb5c6f306 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -6,12 +6,13 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowService } from 'vs/platform/windows/common/windows'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostWindowShape, IExtHostContext, MainContext, MainThreadWindowShape, IOpenUriOptions } from '../common/extHost.protocol'; import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { extractLocalHostUriMetaDataForPortMapping } from 'vs/workbench/contrib/webview/common/portMapping'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; @extHostNamedCustomer(MainContext.MainThreadWindow) export class MainThreadWindow implements MainThreadWindowShape { @@ -23,7 +24,7 @@ export class MainThreadWindow implements MainThreadWindowShape { constructor( extHostContext: IExtHostContext, @IWindowService private readonly windowService: IWindowService, - @IWindowsService private readonly windowsService: IWindowsService, + @IOpenerService private readonly openerService: IOpenerService, @ITunnelService private readonly tunnelService: ITunnelService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { @@ -58,7 +59,7 @@ export class MainThreadWindow implements MainThreadWindowShape { } } - return this.windowsService.openExternal(encodeURI(uri.toString(true))); + return this.openerService.openExternal(uri); } private getOrCreateTunnel(remotePort: number): Promise | undefined { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 99553033cd2..8c903a6027c 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import { originalFSPath } from 'vs/base/common/resources'; +import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { Barrier } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; @@ -332,14 +332,14 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ - this._loadCommonJSModule(extensionDescription.main, activationTimesBuilder), + this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, extensionDescription.main), activationTimesBuilder), this._loadExtensionContext(extensionDescription) ]).then(values => { return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); }); } - protected abstract _loadCommonJSModule(modulePath: string, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise { @@ -536,7 +536,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio let testRunner: ITestRunner | INewTestRunner | undefined; let requireError: Error | undefined; try { - testRunner = await this._loadCommonJSModule(extensionTestsPath, new ExtensionActivationTimesBuilder(false)); + testRunner = await this._loadCommonJSModule(URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false)); } catch (error) { requireError = error; } diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 8f1f5d6ee75..5b03a318564 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -11,6 +11,8 @@ import { connectProxyResolver } from 'vs/workbench/services/extensions/node/prox import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadService'; import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; export class ExtHostExtensionService extends AbstractExtHostExtensionService { @@ -55,12 +57,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { }; } - protected _loadCommonJSModule(modulePath: string, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + if (module.scheme !== Schemas.file) { + throw new Error(`Cannot load URI: '${module}', must be of file-scheme`); + } let r: T | null = null; activationTimesBuilder.codeLoadingStart(); - this._logService.info(`ExtensionService#loadCommonJSModule ${modulePath}`); + this._logService.info(`ExtensionService#loadCommonJSModule ${module.toString(true)}`); try { - r = require.__$__nodeRequire(modulePath); + r = require.__$__nodeRequire(module.fsPath); } catch (e) { return Promise.reject(e); } finally { diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 6f7f2b67feb..0b49644e7cc 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -6,13 +6,16 @@ import { createApiFactoryAndRegisterActors, IExtensionApiFactory } from 'vs/workbench/api/common/extHost.api.impl'; import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator'; import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; -import { endsWith } from 'vs/base/common/strings'; +import { endsWith, startsWith } from 'vs/base/common/strings'; import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import * as vscode from 'vscode'; import { TernarySearchTree } from 'vs/base/common/map'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { joinPath } from 'vs/base/common/resources'; class ApiInstances { @@ -52,11 +55,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { this._apiInstances = new ApiInstances(apiFactory, extensionPath, this._registry, configProvider); } - protected _loadCommonJSModule(modulePath: string, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { - // make sure modulePath ends with `.js` - const suffix = '.js'; - modulePath = endsWith(modulePath, suffix) ? modulePath : modulePath + suffix; interface FakeCommonJSSelf { module?: object; @@ -69,30 +69,66 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // FAKE commonjs world that only collects exports const patchSelf: FakeCommonJSSelf = self; - const module = { exports: {} }; - patchSelf.module = module; - patchSelf.exports = module.exports; patchSelf.window = self; // <- that's improper but might help extensions that aren't authored correctly // FAKE require function that only works for the vscode-module - patchSelf.require = (module: string) => { - if (module !== 'vscode') { - throw new Error(`Cannot load module '${module}'`); + const moduleStack: URI[] = []; + patchSelf.require = (mod: string) => { + const parent = moduleStack[moduleStack.length - 1]; + if (mod === 'vscode') { + return this._apiInstances!.get(parent.fsPath); } - return this._apiInstances!.get(modulePath); + if (!startsWith(mod, '.')) { + throw new Error(`Cannot load module '${mod}'`); + } + + const exports = Object.create(null); + patchSelf.module = { exports }; + patchSelf.exports = exports; + + const next = joinPath(parent, '..', ensureSuffix(mod, '.js')); + moduleStack.push(next); + importScripts(asDomUri(next).toString(true)); + moduleStack.pop(); + + return exports; }; try { activationTimesBuilder.codeLoadingStart(); - importScripts(modulePath); + + const exports = Object.create(null); + patchSelf.module = { exports }; + patchSelf.exports = exports; + + module = module.with({ path: ensureSuffix(module.path, '.js') }); + moduleStack.push(module); + + importScripts(asDomUri(module).toString(true)); + moduleStack.pop(); + } finally { activationTimesBuilder.codeLoadingStop(); } - return Promise.resolve(module.exports as T); + return Promise.resolve(exports); } async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise { throw new Error('Not supported'); } } + +// todo@joh this is a copy of `dom.ts#asDomUri` +function asDomUri(uri: URI): URI { + if (Schemas.vscodeRemote === uri.scheme) { + // rewrite vscode-remote-uris to uris of the window location + // so that they can be intercepted by the service worker + return URI.parse(window.location.href).with({ path: '/vscode-remote', query: JSON.stringify(uri) }); + } + return uri; +} + +function ensureSuffix(path: string, suffix: string): string { + return endsWith(path, suffix) ? path : path + suffix; +} diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 66650e527f7..9e17bf39cc4 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -63,7 +63,7 @@ export class SidebarPart extends CompositePart implements IViewletServi return; } - return width; + return Math.max(width, 300); } //#endregion diff --git a/src/vs/workbench/browser/parts/views/media/panelviewlet.css b/src/vs/workbench/browser/parts/views/media/panelviewlet.css index e1ce2ee1b60..e022f97beb8 100644 --- a/src/vs/workbench/browser/parts/views/media/panelviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/panelviewlet.css @@ -7,6 +7,10 @@ border-top: none !important; /* less clutter: do not show any border for first views in a panel */ } +.monaco-panel-view .panel > .panel-header > .actions.show { + display: initial; +} + .monaco-panel-view .panel > .panel-header h3.title { white-space: nowrap; text-overflow: ellipsis; diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index ef9dbe0d75f..4a5a2a0cbc3 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -41,6 +41,7 @@ export interface IViewletPanelOptions extends IPanelOptions { actionRunner?: IActionRunner; id: string; title: string; + showActionsAlways?: boolean; } export abstract class ViewletPanel extends Panel implements IView { @@ -67,6 +68,7 @@ export abstract class ViewletPanel extends Panel implements IView { protected actionRunner?: IActionRunner; protected toolbar: ToolBar; + private readonly showActionsAlways: boolean = false; private headerContainer: HTMLElement; private titleContainer: HTMLElement; @@ -82,6 +84,7 @@ export abstract class ViewletPanel extends Panel implements IView { this.id = options.id; this.title = options.title; this.actionRunner = options.actionRunner; + this.showActionsAlways = !!options.showActionsAlways; this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); } @@ -133,6 +136,7 @@ export abstract class ViewletPanel extends Panel implements IView { this.renderHeaderTitle(container, this.title); const actions = append(container, $('.actions')); + toggleClass(actions, 'show', this.showActionsAlways); this.toolbar = new ToolBar(actions, this.contextMenuService, { orientation: ActionsOrientation.HORIZONTAL, actionViewItemProvider: action => this.getActionViewItem(action), diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index cf8d773184b..1a47bd47cf8 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -84,11 +84,6 @@ class CodeRendererMain extends Disposable { })); this._register(workbench.onShutdown(() => this.dispose())); - // Driver - if (this.configuration.driver) { - registerWindowDriver(); - } - // Startup workbench.startup(); } diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts index 95d2533c4c6..33737a02966 100644 --- a/src/vs/workbench/browser/web.simpleservices.ts +++ b/src/vs/workbench/browser/web.simpleservices.ts @@ -22,7 +22,7 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; // tslint:disable-next-line: import-patterns -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { addDisposableListener, EventType, windowOpenNoOpener } from 'vs/base/browser/dom'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { pathsToEditors } from 'vs/workbench/common/editor'; @@ -31,14 +31,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { toStoreData, restoreRecentlyOpened } from 'vs/platform/history/common/historyStorage'; -// tslint:disable-next-line: import-patterns -import { IExperimentService, IExperiment, ExperimentActionType, ExperimentState } from 'vs/workbench/contrib/experiments/common/experimentService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProductService } from 'vs/platform/product/common/product'; import Severity from 'vs/base/common/severity'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +// tslint:disable-next-line: import-patterns +import { IWorkspaceStatsService, Tags } from 'vs/workbench/contrib/stats/common/workspaceStats'; //#region Extension Tips @@ -741,13 +741,13 @@ export class SimpleWindowsService implements IWindowsService { async openAboutDialog(): Promise { const detail = localize('aboutDetail', "Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}", - this.productService.version || 'Unknown', - this.productService.commit || 'Unknown', - this.productService.date || 'Unknown', + this.productService.productConfiguration.version || 'Unknown', + this.productService.productConfiguration.commit || 'Unknown', + this.productService.productConfiguration.date || 'Unknown', navigator.userAgent ); - const result = await this.dialogService.show(Severity.Info, this.productService.nameLong, [localize('copy', "Copy"), localize('ok', "OK")], { detail }); + const result = await this.dialogService.show(Severity.Info, this.productService.productConfiguration.nameLong, [localize('copy', "Copy"), localize('ok', "OK")], { detail }); if (result === 0) { this.clipboardService.writeText(detail); @@ -845,33 +845,26 @@ registerSingleton(ITunnelService, SimpleTunnelService); //#endregion -//#region experiments +//#region workspace stats + +class WorkspaceStatsService implements IWorkspaceStatsService { -class ExperimentService implements IExperimentService { _serviceBrand: any; - async getExperimentById(id: string): Promise { - return { - enabled: false, - id: '', - state: ExperimentState.NoRun - }; + getTags(): Promise { + return Promise.resolve({}); } - async getExperimentsByType(type: ExperimentActionType): Promise { - return []; + getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined { + return undefined; } - async getCuratedExtensionsList(curatedExtensionsKey: string): Promise { - return []; + getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit?: boolean): Promise { + return Promise.resolve([]); } - markAsCompleted(experimentId: string): void { } - - onExperimentEnabled: Event = Event.None; - } -registerSingleton(IExperimentService, ExperimentService); +registerSingleton(IWorkspaceStatsService, WorkspaceStatsService); //#endregion diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index 9d6b5c95ba2..a45ab650031 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -7,7 +7,7 @@ import 'vs/css!./accessibility'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { renderFormattedText } from 'vs/base/browser/htmlContentRenderer'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Widget } from 'vs/base/browser/ui/widget'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 7ab435e96ca..5baf982dd93 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; -import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index d4548944019..6fdc99451a6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -175,7 +175,7 @@ export class DebugSession implements IDebugSession { return this.raw!.initialize({ clientID: 'vscode', - clientName: this.productService.nameLong, + clientName: this.productService.productConfiguration.nameLong, adapterID: this.configuration.type, pathFormat: 'path', linesStartAt1: true, diff --git a/src/vs/workbench/contrib/experiments/electron-browser/experimentalPrompt.ts b/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts similarity index 100% rename from src/vs/workbench/contrib/experiments/electron-browser/experimentalPrompt.ts rename to src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts diff --git a/src/vs/workbench/contrib/experiments/electron-browser/experiments.contribution.ts b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts similarity index 79% rename from src/vs/workbench/contrib/experiments/electron-browser/experiments.contribution.ts rename to src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts index ca28e9b6f67..67b6159734f 100644 --- a/src/vs/workbench/contrib/experiments/electron-browser/experiments.contribution.ts +++ b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { ExperimentService } from 'vs/workbench/contrib/experiments/electron-browser/experimentService'; +import { IExperimentService, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/electron-browser/experimentalPrompt'; +import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt'; registerSingleton(IExperimentService, ExperimentService, true); diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index 437afe18bd6..bef55ac4b26 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -4,7 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { language } from 'vs/base/common/platform'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { match } from 'vs/base/common/glob'; +import { IRequestService, asJson } from 'vs/platform/request/common/request'; +import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { distinct } from 'vs/base/common/arrays'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { IProductService } from 'vs/platform/product/common/product'; +import { IWorkspaceStatsService } from 'vs/workbench/contrib/stats/common/workspaceStats'; export const enum ExperimentState { Evaluating, @@ -56,4 +72,388 @@ export interface IExperimentService { onExperimentEnabled: Event; } -export const IExperimentService = createDecorator('experimentService'); \ No newline at end of file +export const IExperimentService = createDecorator('experimentService'); + +interface IExperimentStorageState { + enabled: boolean; + state: ExperimentState; + editCount?: number; + lastEditedDate?: string; +} + +interface IRawExperiment { + id: string; + enabled?: boolean; + condition?: { + insidersOnly?: boolean; + newUser?: boolean; + displayLanguage?: string; + installedExtensions?: { + excludes?: string[]; + includes?: string[]; + }, + fileEdits?: { + filePathPattern?: string; + workspaceIncludes?: string[]; + workspaceExcludes?: string[]; + minEditCount: number; + }, + experimentsPreviouslyRun?: { + excludes?: string[]; + includes?: string[]; + } + userProbability?: number; + }; + action?: IExperimentAction; +} + +export class ExperimentService extends Disposable implements IExperimentService { + _serviceBrand: any; + private _experiments: IExperiment[] = []; + private _loadExperimentsPromise: Promise; + private _curatedMapping = Object.create(null); + + private readonly _onExperimentEnabled = this._register(new Emitter()); + onExperimentEnabled: Event = this._onExperimentEnabled.event; + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @ITextFileService private readonly textFileService: ITextFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IRequestService private readonly requestService: IRequestService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProductService private readonly productService: IProductService, + @IWorkspaceStatsService private readonly workspaceStatsService: IWorkspaceStatsService + ) { + super(); + + this._loadExperimentsPromise = Promise.resolve(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() => this.loadExperiments()); + } + + public getExperimentById(id: string): Promise { + return this._loadExperimentsPromise.then(() => { + return this._experiments.filter(x => x.id === id)[0]; + }); + } + + public getExperimentsByType(type: ExperimentActionType): Promise { + return this._loadExperimentsPromise.then(() => { + if (type === ExperimentActionType.Custom) { + return this._experiments.filter(x => x.enabled && (!x.action || x.action.type === type)); + } + return this._experiments.filter(x => x.enabled && x.action && x.action.type === type); + }); + } + + public getCuratedExtensionsList(curatedExtensionsKey: string): Promise { + return this._loadExperimentsPromise.then(() => { + for (const experiment of this._experiments) { + if (experiment.enabled + && experiment.state === ExperimentState.Run + && this._curatedMapping[experiment.id] + && this._curatedMapping[experiment.id].curatedExtensionsKey === curatedExtensionsKey) { + return this._curatedMapping[experiment.id].curatedExtensionsList; + } + } + return []; + }); + } + + public markAsCompleted(experimentId: string): void { + const storageKey = 'experiments.' + experimentId; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + experimentState.state = ExperimentState.Complete; + this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); + } + + protected getExperiments(): Promise { + if (!this.productService.productConfiguration.experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) { + return Promise.resolve([]); + } + return this.requestService.request({ type: 'GET', url: this.productService.productConfiguration.experimentsUrl }, CancellationToken.None).then(context => { + if (context.res.statusCode !== 200) { + return Promise.resolve(null); + } + return asJson(context).then((result: any) => { + return result && Array.isArray(result['experiments']) ? result['experiments'] : []; + }); + }, () => Promise.resolve(null)); + } + + private loadExperiments(): Promise { + return this.getExperiments().then(rawExperiments => { + // Offline mode + if (!rawExperiments) { + const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); + if (Array.isArray(allExperimentIdsFromStorage)) { + allExperimentIdsFromStorage.forEach(experimentId => { + const storageKey = 'experiments.' + experimentId; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), null); + if (experimentState) { + this._experiments.push({ + id: experimentId, + enabled: experimentState.enabled, + state: experimentState.state + }); + } + }); + } + return Promise.resolve(null); + } + + // Clear disbaled/deleted experiments from storage + const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); + const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase()); + if (Array.isArray(allExperimentIdsFromStorage)) { + allExperimentIdsFromStorage.forEach(experiment => { + if (enabledExperiments.indexOf(experiment) === -1) { + this.storageService.remove(`experiments.${experiment}`, StorageScope.GLOBAL); + } + }); + } + if (enabledExperiments.length) { + this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.GLOBAL); + } else { + this.storageService.remove('allExperiments', StorageScope.GLOBAL); + } + + const promises = rawExperiments.map(experiment => { + const processedExperiment: IExperiment = { + id: experiment.id, + enabled: !!experiment.enabled, + state: !!experiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun + }; + + if (experiment.action) { + processedExperiment.action = { + type: ExperimentActionType[experiment.action.type] || ExperimentActionType.Custom, + properties: experiment.action.properties + }; + if (processedExperiment.action.type === ExperimentActionType.Prompt) { + ((processedExperiment.action.properties).commands || []).forEach(x => { + if (x.curatedExtensionsKey && Array.isArray(x.curatedExtensionsList)) { + this._curatedMapping[experiment.id] = x; + } + }); + } + if (!processedExperiment.action.properties) { + processedExperiment.action.properties = {}; + } + } + this._experiments.push(processedExperiment); + + if (!processedExperiment.enabled) { + return Promise.resolve(null); + } + + const storageKey = 'experiments.' + experiment.id; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + if (!experimentState.hasOwnProperty('enabled')) { + experimentState.enabled = processedExperiment.enabled; + } + if (!experimentState.hasOwnProperty('state')) { + experimentState.state = processedExperiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun; + } else { + processedExperiment.state = experimentState.state; + } + + return this.shouldRunExperiment(experiment, processedExperiment).then((state: ExperimentState) => { + experimentState.state = processedExperiment.state = state; + this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); + + if (state === ExperimentState.Run) { + this.fireRunExperiment(processedExperiment); + } + return Promise.resolve(null); + }); + + }); + return Promise.all(promises).then(() => { + type ExperimentsClassification = { + experiments: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + }; + this.telemetryService.publicLog2<{ experiments: IExperiment[] }, ExperimentsClassification>('experiments', { experiments: this._experiments }); + }); + }); + } + + private fireRunExperiment(experiment: IExperiment) { + this._onExperimentEnabled.fire(experiment); + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + if (runExperimentIdsFromStorage.indexOf(experiment.id) === -1) { + runExperimentIdsFromStorage.push(experiment.id); + } + + // Ensure we dont store duplicates + const distinctExperiments = distinct(runExperimentIdsFromStorage); + if (runExperimentIdsFromStorage.length !== distinctExperiments.length) { + this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(distinctExperiments), StorageScope.GLOBAL); + } + } + + private checkExperimentDependencies(experiment: IRawExperiment): boolean { + const experimentsPreviouslyRun = experiment.condition ? experiment.condition.experimentsPreviouslyRun : undefined; + if (experimentsPreviouslyRun) { + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + let includeCheck = true; + let excludeCheck = true; + const includes = experimentsPreviouslyRun.includes; + if (Array.isArray(includes)) { + includeCheck = runExperimentIdsFromStorage.some(x => includes.indexOf(x) > -1); + } + const excludes = experimentsPreviouslyRun.excludes; + if (includeCheck && Array.isArray(excludes)) { + excludeCheck = !runExperimentIdsFromStorage.some(x => excludes.indexOf(x) > -1); + } + if (!includeCheck || !excludeCheck) { + return false; + } + } + return true; + } + + private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): Promise { + if (processedExperiment.state !== ExperimentState.Evaluating) { + return Promise.resolve(processedExperiment.state); + } + + if (!experiment.enabled) { + return Promise.resolve(ExperimentState.NoRun); + } + + const condition = experiment.condition; + if (!condition) { + return Promise.resolve(ExperimentState.Run); + } + + if (!this.checkExperimentDependencies(experiment)) { + return Promise.resolve(ExperimentState.NoRun); + } + + if (this.environmentService.appQuality === 'stable' && condition.insidersOnly === true) { + return Promise.resolve(ExperimentState.NoRun); + } + + const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL); + if ((condition.newUser === true && !isNewUser) + || (condition.newUser === false && isNewUser)) { + return Promise.resolve(ExperimentState.NoRun); + } + + if (typeof condition.displayLanguage === 'string') { + let localeToCheck = condition.displayLanguage.toLowerCase(); + let displayLanguage = language!.toLowerCase(); + + if (localeToCheck !== displayLanguage) { + const a = displayLanguage.indexOf('-'); + const b = localeToCheck.indexOf('-'); + if (a > -1) { + displayLanguage = displayLanguage.substr(0, a); + } + if (b > -1) { + localeToCheck = localeToCheck.substr(0, b); + } + if (displayLanguage !== localeToCheck) { + return Promise.resolve(ExperimentState.NoRun); + } + } + } + + if (!condition.userProbability) { + condition.userProbability = 1; + } + + let extensionsCheckPromise = Promise.resolve(true); + const installedExtensions = condition.installedExtensions; + if (installedExtensions) { + extensionsCheckPromise = this.extensionManagementService.getInstalled(ExtensionType.User).then(locals => { + let includesCheck = true; + let excludesCheck = true; + const localExtensions = locals.map(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}`); + if (Array.isArray(installedExtensions.includes) && installedExtensions.includes.length) { + const extensionIncludes = installedExtensions.includes.map(e => e.toLowerCase()); + includesCheck = localExtensions.some(e => extensionIncludes.indexOf(e) > -1); + } + if (Array.isArray(installedExtensions.excludes) && installedExtensions.excludes.length) { + const extensionExcludes = installedExtensions.excludes.map(e => e.toLowerCase()); + excludesCheck = !localExtensions.some(e => extensionExcludes.indexOf(e) > -1); + } + return includesCheck && excludesCheck; + }); + } + + const storageKey = 'experiments.' + experiment.id; + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + + return extensionsCheckPromise.then(success => { + const fileEdits = condition.fileEdits; + if (!success || !fileEdits || typeof fileEdits.minEditCount !== 'number') { + const runExperiment = success && typeof condition.userProbability === 'number' && Math.random() < condition.userProbability; + return runExperiment ? ExperimentState.Run : ExperimentState.NoRun; + } + + experimentState.editCount = experimentState.editCount || 0; + if (experimentState.editCount >= fileEdits.minEditCount) { + return ExperimentState.Run; + } + + const onSaveHandler = this.textFileService.models.onModelsSaved(e => { + const date = new Date().toDateString(); + const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + if (latestExperimentState.state !== ExperimentState.Evaluating) { + onSaveHandler.dispose(); + return; + } + e.forEach(async event => { + if (event.kind !== StateChange.SAVED + || latestExperimentState.state !== ExperimentState.Evaluating + || date === latestExperimentState.lastEditedDate + || (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) + ) { + return; + } + let filePathCheck = true; + let workspaceCheck = true; + + if (typeof fileEdits.filePathPattern === 'string') { + filePathCheck = match(fileEdits.filePathPattern, event.resource.fsPath); + } + if (Array.isArray(fileEdits.workspaceIncludes) && fileEdits.workspaceIncludes.length) { + const tags = await this.workspaceStatsService.getTags(); + workspaceCheck = !!tags && fileEdits.workspaceIncludes.some(x => !!tags[x]); + } + if (workspaceCheck && Array.isArray(fileEdits.workspaceExcludes) && fileEdits.workspaceExcludes.length) { + const tags = await this.workspaceStatsService.getTags(); + workspaceCheck = !!tags && !fileEdits.workspaceExcludes.some(x => !!tags[x]); + } + if (filePathCheck && workspaceCheck) { + latestExperimentState.editCount = (latestExperimentState.editCount || 0) + 1; + latestExperimentState.lastEditedDate = date; + this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); + } + }); + if (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) { + processedExperiment.state = latestExperimentState.state = (typeof condition.userProbability === 'number' && Math.random() < condition.userProbability && this.checkExperimentDependencies(experiment)) ? ExperimentState.Run : ExperimentState.NoRun; + this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); + if (latestExperimentState.state === ExperimentState.Run && experiment.action && ExperimentActionType[experiment.action.type] === ExperimentActionType.Prompt) { + this.fireRunExperiment(processedExperiment); + } + } + }); + this._register(onSaveHandler); + return ExperimentState.Evaluating; + }); + } +} + + +function safeParse(text: string | undefined, defaultObject: any) { + try { + return text ? JSON.parse(text) || defaultObject : defaultObject; + } catch (e) { + return defaultObject; + } +} diff --git a/src/vs/workbench/contrib/experiments/electron-browser/experimentService.ts b/src/vs/workbench/contrib/experiments/electron-browser/experimentService.ts deleted file mode 100644 index 79dd609cb60..00000000000 --- a/src/vs/workbench/contrib/experiments/electron-browser/experimentService.ts +++ /dev/null @@ -1,407 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { language } from 'vs/base/common/platform'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { match } from 'vs/base/common/glob'; -import { IRequestService, asJson } from 'vs/platform/request/common/request'; -import { Emitter, Event } from 'vs/base/common/event'; -import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { distinct } from 'vs/base/common/arrays'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { ExperimentState, IExperimentAction, IExperimentService, IExperiment, ExperimentActionType, IExperimentActionPromptProperties } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { IProductService } from 'vs/platform/product/common/product'; -import { IWorkspaceStatsService } from 'vs/workbench/contrib/stats/electron-browser/workspaceStatsService'; - -interface IExperimentStorageState { - enabled: boolean; - state: ExperimentState; - editCount?: number; - lastEditedDate?: string; -} - -interface IRawExperiment { - id: string; - enabled?: boolean; - condition?: { - insidersOnly?: boolean; - newUser?: boolean; - displayLanguage?: string; - installedExtensions?: { - excludes?: string[]; - includes?: string[]; - }, - fileEdits?: { - filePathPattern?: string; - workspaceIncludes?: string[]; - workspaceExcludes?: string[]; - minEditCount: number; - }, - experimentsPreviouslyRun?: { - excludes?: string[]; - includes?: string[]; - } - userProbability?: number; - }; - action?: IExperimentAction; -} - -export class ExperimentService extends Disposable implements IExperimentService { - _serviceBrand: any; - private _experiments: IExperiment[] = []; - private _loadExperimentsPromise: Promise; - private _curatedMapping = Object.create(null); - - private readonly _onExperimentEnabled = this._register(new Emitter()); - onExperimentEnabled: Event = this._onExperimentEnabled.event; - - constructor( - @IStorageService private readonly storageService: IStorageService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @ITextFileService private readonly textFileService: ITextFileService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IRequestService private readonly requestService: IRequestService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IProductService private readonly productService: IProductService, - @IWorkspaceStatsService private readonly workspaceStatsService: IWorkspaceStatsService - ) { - super(); - - this._loadExperimentsPromise = Promise.resolve(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() => this.loadExperiments()); - } - - public getExperimentById(id: string): Promise { - return this._loadExperimentsPromise.then(() => { - return this._experiments.filter(x => x.id === id)[0]; - }); - } - - public getExperimentsByType(type: ExperimentActionType): Promise { - return this._loadExperimentsPromise.then(() => { - if (type === ExperimentActionType.Custom) { - return this._experiments.filter(x => x.enabled && (!x.action || x.action.type === type)); - } - return this._experiments.filter(x => x.enabled && x.action && x.action.type === type); - }); - } - - public getCuratedExtensionsList(curatedExtensionsKey: string): Promise { - return this._loadExperimentsPromise.then(() => { - for (const experiment of this._experiments) { - if (experiment.enabled - && experiment.state === ExperimentState.Run - && this._curatedMapping[experiment.id] - && this._curatedMapping[experiment.id].curatedExtensionsKey === curatedExtensionsKey) { - return this._curatedMapping[experiment.id].curatedExtensionsList; - } - } - return []; - }); - } - - public markAsCompleted(experimentId: string): void { - const storageKey = 'experiments.' + experimentId; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); - experimentState.state = ExperimentState.Complete; - this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); - } - - protected getExperiments(): Promise { - if (!this.productService.experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) { - return Promise.resolve([]); - } - return this.requestService.request({ type: 'GET', url: this.productService.experimentsUrl }, CancellationToken.None).then(context => { - if (context.res.statusCode !== 200) { - return Promise.resolve(null); - } - return asJson(context).then((result: any) => { - return result && Array.isArray(result['experiments']) ? result['experiments'] : []; - }); - }, () => Promise.resolve(null)); - } - - private loadExperiments(): Promise { - return this.getExperiments().then(rawExperiments => { - // Offline mode - if (!rawExperiments) { - const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); - if (Array.isArray(allExperimentIdsFromStorage)) { - allExperimentIdsFromStorage.forEach(experimentId => { - const storageKey = 'experiments.' + experimentId; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), null); - if (experimentState) { - this._experiments.push({ - id: experimentId, - enabled: experimentState.enabled, - state: experimentState.state - }); - } - }); - } - return Promise.resolve(null); - } - - // Clear disbaled/deleted experiments from storage - const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); - const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase()); - if (Array.isArray(allExperimentIdsFromStorage)) { - allExperimentIdsFromStorage.forEach(experiment => { - if (enabledExperiments.indexOf(experiment) === -1) { - this.storageService.remove(`experiments.${experiment}`, StorageScope.GLOBAL); - } - }); - } - if (enabledExperiments.length) { - this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.GLOBAL); - } else { - this.storageService.remove('allExperiments', StorageScope.GLOBAL); - } - - const promises = rawExperiments.map(experiment => { - const processedExperiment: IExperiment = { - id: experiment.id, - enabled: !!experiment.enabled, - state: !!experiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun - }; - - if (experiment.action) { - processedExperiment.action = { - type: ExperimentActionType[experiment.action.type] || ExperimentActionType.Custom, - properties: experiment.action.properties - }; - if (processedExperiment.action.type === ExperimentActionType.Prompt) { - ((processedExperiment.action.properties).commands || []).forEach(x => { - if (x.curatedExtensionsKey && Array.isArray(x.curatedExtensionsList)) { - this._curatedMapping[experiment.id] = x; - } - }); - } - if (!processedExperiment.action.properties) { - processedExperiment.action.properties = {}; - } - } - this._experiments.push(processedExperiment); - - if (!processedExperiment.enabled) { - return Promise.resolve(null); - } - - const storageKey = 'experiments.' + experiment.id; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); - if (!experimentState.hasOwnProperty('enabled')) { - experimentState.enabled = processedExperiment.enabled; - } - if (!experimentState.hasOwnProperty('state')) { - experimentState.state = processedExperiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun; - } else { - processedExperiment.state = experimentState.state; - } - - return this.shouldRunExperiment(experiment, processedExperiment).then((state: ExperimentState) => { - experimentState.state = processedExperiment.state = state; - this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL); - - if (state === ExperimentState.Run) { - this.fireRunExperiment(processedExperiment); - } - return Promise.resolve(null); - }); - - }); - return Promise.all(promises).then(() => { - type ExperimentsClassification = { - experiments: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - this.telemetryService.publicLog2<{ experiments: IExperiment[] }, ExperimentsClassification>('experiments', { experiments: this._experiments }); - }); - }); - } - - private fireRunExperiment(experiment: IExperiment) { - this._onExperimentEnabled.fire(experiment); - const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); - if (runExperimentIdsFromStorage.indexOf(experiment.id) === -1) { - runExperimentIdsFromStorage.push(experiment.id); - } - - // Ensure we dont store duplicates - const distinctExperiments = distinct(runExperimentIdsFromStorage); - if (runExperimentIdsFromStorage.length !== distinctExperiments.length) { - this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(distinctExperiments), StorageScope.GLOBAL); - } - } - - private checkExperimentDependencies(experiment: IRawExperiment): boolean { - const experimentsPreviouslyRun = experiment.condition ? experiment.condition.experimentsPreviouslyRun : undefined; - if (experimentsPreviouslyRun) { - const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); - let includeCheck = true; - let excludeCheck = true; - const includes = experimentsPreviouslyRun.includes; - if (Array.isArray(includes)) { - includeCheck = runExperimentIdsFromStorage.some(x => includes.indexOf(x) > -1); - } - const excludes = experimentsPreviouslyRun.excludes; - if (includeCheck && Array.isArray(excludes)) { - excludeCheck = !runExperimentIdsFromStorage.some(x => excludes.indexOf(x) > -1); - } - if (!includeCheck || !excludeCheck) { - return false; - } - } - return true; - } - - private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): Promise { - if (processedExperiment.state !== ExperimentState.Evaluating) { - return Promise.resolve(processedExperiment.state); - } - - if (!experiment.enabled) { - return Promise.resolve(ExperimentState.NoRun); - } - - const condition = experiment.condition; - if (!condition) { - return Promise.resolve(ExperimentState.Run); - } - - if (!this.checkExperimentDependencies(experiment)) { - return Promise.resolve(ExperimentState.NoRun); - } - - if (this.environmentService.appQuality === 'stable' && condition.insidersOnly === true) { - return Promise.resolve(ExperimentState.NoRun); - } - - const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL); - if ((condition.newUser === true && !isNewUser) - || (condition.newUser === false && isNewUser)) { - return Promise.resolve(ExperimentState.NoRun); - } - - if (typeof condition.displayLanguage === 'string') { - let localeToCheck = condition.displayLanguage.toLowerCase(); - let displayLanguage = language!.toLowerCase(); - - if (localeToCheck !== displayLanguage) { - const a = displayLanguage.indexOf('-'); - const b = localeToCheck.indexOf('-'); - if (a > -1) { - displayLanguage = displayLanguage.substr(0, a); - } - if (b > -1) { - localeToCheck = localeToCheck.substr(0, b); - } - if (displayLanguage !== localeToCheck) { - return Promise.resolve(ExperimentState.NoRun); - } - } - } - - if (!condition.userProbability) { - condition.userProbability = 1; - } - - let extensionsCheckPromise = Promise.resolve(true); - const installedExtensions = condition.installedExtensions; - if (installedExtensions) { - extensionsCheckPromise = this.extensionManagementService.getInstalled(ExtensionType.User).then(locals => { - let includesCheck = true; - let excludesCheck = true; - const localExtensions = locals.map(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}`); - if (Array.isArray(installedExtensions.includes) && installedExtensions.includes.length) { - const extensionIncludes = installedExtensions.includes.map(e => e.toLowerCase()); - includesCheck = localExtensions.some(e => extensionIncludes.indexOf(e) > -1); - } - if (Array.isArray(installedExtensions.excludes) && installedExtensions.excludes.length) { - const extensionExcludes = installedExtensions.excludes.map(e => e.toLowerCase()); - excludesCheck = !localExtensions.some(e => extensionExcludes.indexOf(e) > -1); - } - return includesCheck && excludesCheck; - }); - } - - const storageKey = 'experiments.' + experiment.id; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); - - return extensionsCheckPromise.then(success => { - const fileEdits = condition.fileEdits; - if (!success || !fileEdits || typeof fileEdits.minEditCount !== 'number') { - const runExperiment = success && typeof condition.userProbability === 'number' && Math.random() < condition.userProbability; - return runExperiment ? ExperimentState.Run : ExperimentState.NoRun; - } - - experimentState.editCount = experimentState.editCount || 0; - if (experimentState.editCount >= fileEdits.minEditCount) { - return ExperimentState.Run; - } - - const onSaveHandler = this.textFileService.models.onModelsSaved(e => { - const date = new Date().toDateString(); - const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); - if (latestExperimentState.state !== ExperimentState.Evaluating) { - onSaveHandler.dispose(); - return; - } - e.forEach(async event => { - if (event.kind !== StateChange.SAVED - || latestExperimentState.state !== ExperimentState.Evaluating - || date === latestExperimentState.lastEditedDate - || (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) - ) { - return; - } - let filePathCheck = true; - let workspaceCheck = true; - - if (typeof fileEdits.filePathPattern === 'string') { - filePathCheck = match(fileEdits.filePathPattern, event.resource.fsPath); - } - if (Array.isArray(fileEdits.workspaceIncludes) && fileEdits.workspaceIncludes.length) { - const tags = await this.workspaceStatsService.getTags(); - workspaceCheck = !!tags && fileEdits.workspaceIncludes.some(x => !!tags[x]); - } - if (workspaceCheck && Array.isArray(fileEdits.workspaceExcludes) && fileEdits.workspaceExcludes.length) { - const tags = await this.workspaceStatsService.getTags(); - workspaceCheck = !!tags && !fileEdits.workspaceExcludes.some(x => !!tags[x]); - } - if (filePathCheck && workspaceCheck) { - latestExperimentState.editCount = (latestExperimentState.editCount || 0) + 1; - latestExperimentState.lastEditedDate = date; - this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); - } - }); - if (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) { - processedExperiment.state = latestExperimentState.state = (typeof condition.userProbability === 'number' && Math.random() < condition.userProbability && this.checkExperimentDependencies(experiment)) ? ExperimentState.Run : ExperimentState.NoRun; - this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL); - if (latestExperimentState.state === ExperimentState.Run && experiment.action && ExperimentActionType[experiment.action.type] === ExperimentActionType.Prompt) { - this.fireRunExperiment(processedExperiment); - } - } - }); - this._register(onSaveHandler); - return ExperimentState.Evaluating; - }); - } -} - - -function safeParse(text: string | undefined, defaultObject: any) { - try { - return text ? JSON.parse(text) || defaultObject : defaultObject; - } catch (e) { - return defaultObject; - } -} diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index f44b4d3904e..70851b908c9 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ExperimentActionType, ExperimentState, IExperiment } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { ExperimentService } from 'vs/workbench/contrib/experiments/electron-browser/experimentService'; +import { ExperimentActionType, ExperimentState, IExperiment, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts index 781fc5ab7c0..ff693a1f4e3 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentalPrompts.test.ts @@ -12,7 +12,7 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/electron-browser/experimentalPrompt'; +import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt'; import { ExperimentActionType, ExperimentState, IExperiment, IExperimentActionPromptProperties, IExperimentService, LocalizedPromptText } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index e7cc0836f23..2a19adea3df 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -133,29 +133,33 @@ interface IActiveElement { focus(): void; } +interface IExtensionEditorTemplate { + iconContainer: HTMLElement; + icon: HTMLImageElement; + name: HTMLElement; + identifier: HTMLElement; + preview: HTMLElement; + builtin: HTMLElement; + license: HTMLElement; + publisher: HTMLElement; + installCount: HTMLElement; + rating: HTMLElement; + repository: HTMLElement; + description: HTMLElement; + extensionActionBar: ActionBar; + navbar: NavBar; + content: HTMLElement; + subtextContainer: HTMLElement; + subtext: HTMLElement; + ignoreActionbar: ActionBar; + header: HTMLElement; +} + export class ExtensionEditor extends BaseEditor { static readonly ID: string = 'workbench.editor.extension'; - private iconContainer: HTMLElement; - private icon: HTMLImageElement; - private name: HTMLElement; - private identifier: HTMLElement; - private preview: HTMLElement; - private builtin: HTMLElement; - private license: HTMLElement; - private publisher: HTMLElement; - private installCount: HTMLElement; - private rating: HTMLElement; - private repository: HTMLElement; - private description: HTMLElement; - private extensionActionBar: ActionBar; - private navbar: NavBar; - private content: HTMLElement; - private subtextContainer: HTMLElement; - private subtext: HTMLElement; - private ignoreActionbar: ActionBar; - private header: HTMLElement; + private template: IExtensionEditorTemplate | undefined; private extensionReadme: Cache | null; private extensionChangelog: Cache | null; @@ -164,7 +168,7 @@ export class ExtensionEditor extends BaseEditor { private layoutParticipants: ILayoutParticipant[] = []; private readonly contentDisposables = this._register(new DisposableStore()); private readonly transientDisposables = this._register(new DisposableStore()); - private activeElement: IActiveElement | null; + private activeElement: IActiveElement | null = null; private editorLoadComplete: boolean = false; constructor( @@ -192,43 +196,43 @@ export class ExtensionEditor extends BaseEditor { const root = append(parent, $('.extension-editor')); root.tabIndex = 0; // this is required for the focus tracker on the editor root.style.outline = 'none'; - this.header = append(root, $('.header')); + const header = append(root, $('.header')); - this.iconContainer = append(this.header, $('.icon-container')); - this.icon = append(this.iconContainer, $('img.icon', { draggable: false })); + const iconContainer = append(header, $('.icon-container')); + const icon = append(iconContainer, $('img.icon', { draggable: false })); - const details = append(this.header, $('.details')); + const details = append(header, $('.details')); const title = append(details, $('.title')); - this.name = append(title, $('span.name.clickable', { title: localize('name', "Extension name") })); - this.identifier = append(title, $('span.identifier', { title: localize('extension id', "Extension identifier") })); + const name = append(title, $('span.name.clickable', { title: localize('name', "Extension name") })); + const identifier = append(title, $('span.identifier', { title: localize('extension id', "Extension identifier") })); - this.preview = append(title, $('span.preview', { title: localize('preview', "Preview") })); - this.preview.textContent = localize('preview', "Preview"); + const preview = append(title, $('span.preview', { title: localize('preview', "Preview") })); + preview.textContent = localize('preview', "Preview"); - this.builtin = append(title, $('span.builtin')); - this.builtin.textContent = localize('builtin', "Built-in"); + const builtin = append(title, $('span.builtin')); + builtin.textContent = localize('builtin', "Built-in"); const subtitle = append(details, $('.subtitle')); - this.publisher = append(subtitle, $('span.publisher.clickable', { title: localize('publisher', "Publisher name"), tabIndex: 0 })); + const publisher = append(subtitle, $('span.publisher.clickable', { title: localize('publisher', "Publisher name"), tabIndex: 0 })); - this.installCount = append(subtitle, $('span.install', { title: localize('install count', "Install count"), tabIndex: 0 })); + const installCount = append(subtitle, $('span.install', { title: localize('install count', "Install count"), tabIndex: 0 })); - this.rating = append(subtitle, $('span.rating.clickable', { title: localize('rating', "Rating"), tabIndex: 0 })); + const rating = append(subtitle, $('span.rating.clickable', { title: localize('rating', "Rating"), tabIndex: 0 })); - this.repository = append(subtitle, $('span.repository.clickable')); - this.repository.textContent = localize('repository', 'Repository'); - this.repository.style.display = 'none'; - this.repository.tabIndex = 0; + const repository = append(subtitle, $('span.repository.clickable')); + repository.textContent = localize('repository', 'Repository'); + repository.style.display = 'none'; + repository.tabIndex = 0; - this.license = append(subtitle, $('span.license.clickable')); - this.license.textContent = localize('license', 'License'); - this.license.style.display = 'none'; - this.license.tabIndex = 0; + const license = append(subtitle, $('span.license.clickable')); + license.textContent = localize('license', 'License'); + license.style.display = 'none'; + license.tabIndex = 0; - this.description = append(details, $('.description')); + const description = append(details, $('.description')); const extensionActions = append(details, $('.actions')); - this.extensionActionBar = new ActionBar(extensionActions, { + const extensionActionBar = this._register(new ActionBar(extensionActions, { animated: false, actionViewItemProvider: (action: Action) => { if (action instanceof ExtensionEditorDropDownAction) { @@ -236,29 +240,48 @@ export class ExtensionEditor extends BaseEditor { } return undefined; } - }); + })); - this.subtextContainer = append(details, $('.subtext-container')); - this.subtext = append(this.subtextContainer, $('.subtext')); - this.ignoreActionbar = new ActionBar(this.subtextContainer, { animated: false }); + const subtextContainer = append(details, $('.subtext-container')); + const subtext = append(subtextContainer, $('.subtext')); + const ignoreActionbar = this._register(new ActionBar(subtextContainer, { animated: false })); - this._register(this.extensionActionBar); - this._register(this.ignoreActionbar); - - this._register(Event.chain(this.extensionActionBar.onDidRun) + this._register(Event.chain(extensionActionBar.onDidRun) .map(({ error }) => error) .filter(error => !!error) .on(this.onError, this)); - this._register(Event.chain(this.ignoreActionbar.onDidRun) + this._register(Event.chain(ignoreActionbar.onDidRun) .map(({ error }) => error) .filter(error => !!error) .on(this.onError, this)); const body = append(root, $('.body')); - this.navbar = new NavBar(body); + const navbar = new NavBar(body); - this.content = append(body, $('.content')); + const content = append(body, $('.content')); + + this.template = { + builtin, + content, + description, + extensionActionBar, + header, + icon, + iconContainer, + identifier, + ignoreActionbar, + installCount, + license, + name, + navbar, + preview, + publisher, + rating, + repository, + subtext, + subtextContainer + }; } private onClick(element: HTMLElement, callback: () => void): IDisposable { @@ -276,6 +299,13 @@ export class ExtensionEditor extends BaseEditor { } async setInput(input: ExtensionsInput, options: EditorOptions, token: CancellationToken): Promise { + if (this.template) { + await this.updateTemplate(input, this.template); + } + return super.setInput(input, options, token); + } + + private async updateTemplate(input: ExtensionsInput, template: IExtensionEditorTemplate): Promise { const runningExtensions = await this.extensionService.getExtensions(); const colorThemes = await this.workbenchThemeService.getColorThemes(); const fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); @@ -290,18 +320,18 @@ export class ExtensionEditor extends BaseEditor { this.extensionChangelog = new Cache(() => createCancelablePromise(token => extension.getChangelog(token))); this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(token))); - const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, this.iconContainer, true); - const onError = Event.once(domEvent(this.icon, 'error')); - onError(() => this.icon.src = extension.iconUrlFallback, null, this.transientDisposables); - this.icon.src = extension.iconUrl; + const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, template.iconContainer, true); + const onError = Event.once(domEvent(template.icon, 'error')); + onError(() => template.icon.src = extension.iconUrlFallback, null, this.transientDisposables); + template.icon.src = extension.iconUrl; - this.name.textContent = extension.displayName; - this.identifier.textContent = extension.identifier.id; - this.preview.style.display = extension.preview ? 'inherit' : 'none'; - this.builtin.style.display = extension.type === ExtensionType.System ? 'inherit' : 'none'; + template.name.textContent = extension.displayName; + template.identifier.textContent = extension.identifier.id; + template.preview.style.display = extension.preview ? 'inherit' : 'none'; + template.builtin.style.display = extension.type === ExtensionType.System ? 'inherit' : 'none'; - this.publisher.textContent = extension.publisherDisplayName; - this.description.textContent = extension.description; + template.publisher.textContent = extension.publisherDisplayName; + template.description.textContent = extension.description; const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); let recommendationsData = {}; @@ -319,40 +349,40 @@ export class ExtensionEditor extends BaseEditor { */ this.telemetryService.publicLog('extensionGallery:openExtension', assign(extension.telemetryData, recommendationsData)); - toggleClass(this.name, 'clickable', !!extension.url); - toggleClass(this.publisher, 'clickable', !!extension.url); - toggleClass(this.rating, 'clickable', !!extension.url); + toggleClass(template.name, 'clickable', !!extension.url); + toggleClass(template.publisher, 'clickable', !!extension.url); + toggleClass(template.rating, 'clickable', !!extension.url); if (extension.url) { - this.transientDisposables.add(this.onClick(this.name, () => window.open(extension.url))); - this.transientDisposables.add(this.onClick(this.rating, () => window.open(`${extension.url}#review-details`))); - this.transientDisposables.add(this.onClick(this.publisher, () => { + this.transientDisposables.add(this.onClick(template.name, () => window.open(extension.url))); + this.transientDisposables.add(this.onClick(template.rating, () => window.open(`${extension.url}#review-details`))); + this.transientDisposables.add(this.onClick(template.publisher, () => { this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet as IExtensionsViewlet) .then(viewlet => viewlet.search(`publisher:"${extension.publisherDisplayName}"`)); })); if (extension.licenseUrl) { - this.transientDisposables.add(this.onClick(this.license, () => window.open(extension.licenseUrl))); - this.license.style.display = 'initial'; + this.transientDisposables.add(this.onClick(template.license, () => window.open(extension.licenseUrl))); + template.license.style.display = 'initial'; } else { - this.license.style.display = 'none'; + template.license.style.display = 'none'; } } else { - this.license.style.display = 'none'; + template.license.style.display = 'none'; } if (extension.repository) { - this.transientDisposables.add(this.onClick(this.repository, () => window.open(extension.repository))); - this.repository.style.display = 'initial'; + this.transientDisposables.add(this.onClick(template.repository, () => window.open(extension.repository))); + template.repository.style.display = 'initial'; } else { - this.repository.style.display = 'none'; + template.repository.style.display = 'none'; } const widgets = [ remoteBadge, - this.instantiationService.createInstance(InstallCountWidget, this.installCount, false), - this.instantiationService.createInstance(RatingsWidget, this.rating, false) + this.instantiationService.createInstance(InstallCountWidget, template.installCount, false), + this.instantiationService.createInstance(RatingsWidget, template.rating, false) ]; const reloadAction = this.instantiationService.createInstance(ReloadAction); const combinedInstallAction = this.instantiationService.createInstance(CombinedInstallAction); @@ -375,20 +405,20 @@ export class ExtensionEditor extends BaseEditor { const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); extensionContainers.extension = extension; - this.extensionActionBar.clear(); - this.extensionActionBar.push(actions, { icon: true, label: true }); + template.extensionActionBar.clear(); + template.extensionActionBar.push(actions, { icon: true, label: true }); for (const disposable of [...actions, ...widgets, extensionContainers]) { this.transientDisposables.add(disposable); } - this.setSubText(extension, reloadAction); - this.content.innerHTML = ''; // Clear content before setting navbar actions. + this.setSubText(extension, reloadAction, template); + template.content.innerHTML = ''; // Clear content before setting navbar actions. - this.navbar.clear(); - this.navbar.onChange(this.onNavbarChange.bind(this, extension), this, this.transientDisposables); + template.navbar.clear(); + template.navbar.onChange(e => this.onNavbarChange(extension, e, template), this, this.transientDisposables); if (extension.hasReadme()) { - this.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); + template.navbar.push(NavbarSection.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file")); } this.extensionManifest.get() .promise @@ -397,25 +427,23 @@ export class ExtensionEditor extends BaseEditor { combinedInstallAction.manifest = manifest; } if (extension.extensionPack.length) { - this.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); + template.navbar.push(NavbarSection.ExtensionPack, localize('extensionPack', "Extension Pack"), localize('extensionsPack', "Set of extensions that can be installed together")); } if (manifest && manifest.contributes) { - this.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); + template.navbar.push(NavbarSection.Contributions, localize('contributions', "Contributions"), localize('contributionstooltip', "Lists contributions to VS Code by this extension")); } if (extension.hasChangelog()) { - this.navbar.push(NavbarSection.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file")); + template.navbar.push(NavbarSection.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file")); } if (extension.dependencies.length) { - this.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); + template.navbar.push(NavbarSection.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on")); } this.editorLoadComplete = true; }); - - return super.setInput(input, options, token); } - private setSubText(extension: IExtension, reloadAction: ReloadAction): void { - hide(this.subtextContainer); + private setSubText(extension: IExtension, reloadAction: ReloadAction, template: IExtensionEditorTemplate): void { + hide(template.subtextContainer); const ignoreAction = this.instantiationService.createInstance(IgnoreExtensionRecommendationAction); const undoIgnoreAction = this.instantiationService.createInstance(UndoIgnoreExtensionRecommendationAction); @@ -424,23 +452,23 @@ export class ExtensionEditor extends BaseEditor { ignoreAction.enabled = false; undoIgnoreAction.enabled = false; - this.ignoreActionbar.clear(); - this.ignoreActionbar.push([ignoreAction, undoIgnoreAction], { icon: true, label: true }); + template.ignoreActionbar.clear(); + template.ignoreActionbar.push([ignoreAction, undoIgnoreAction], { icon: true, label: true }); this.transientDisposables.add(ignoreAction); this.transientDisposables.add(undoIgnoreAction); const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); if (extRecommendations[extension.identifier.id.toLowerCase()]) { ignoreAction.enabled = true; - this.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; - show(this.subtextContainer); + template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; + show(template.subtextContainer); } else if (this.extensionTipsService.getAllIgnoredRecommendations().global.indexOf(extension.identifier.id.toLowerCase()) !== -1) { undoIgnoreAction.enabled = true; - this.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); - show(this.subtextContainer); + template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); + show(template.subtextContainer); } else { - this.subtext.textContent = ''; + template.subtext.textContent = ''; } this.extensionTipsService.onRecommendationChange(change => { @@ -450,28 +478,28 @@ export class ExtensionEditor extends BaseEditor { const extRecommendations = this.extensionTipsService.getAllRecommendationsWithReason(); if (extRecommendations[extension.identifier.id.toLowerCase()]) { ignoreAction.enabled = true; - this.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; + template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; } } else { undoIgnoreAction.enabled = true; ignoreAction.enabled = false; - this.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); + template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."); } } }); this.transientDisposables.add(reloadAction.onDidChange(e => { if (e.tooltip) { - this.subtext.textContent = reloadAction.tooltip; - show(this.subtextContainer); + template.subtext.textContent = reloadAction.tooltip; + show(template.subtextContainer); ignoreAction.enabled = false; undoIgnoreAction.enabled = false; } if (e.enabled === true) { - show(this.subtextContainer); + show(template.subtextContainer); } if (e.enabled === false) { - hide(this.subtextContainer); + hide(template.subtextContainer); } })); } @@ -495,7 +523,7 @@ export class ExtensionEditor extends BaseEditor { } } - private onNavbarChange(extension: IExtension, { id, focus }: { id: string, focus: boolean }): void { + private onNavbarChange(extension: IExtension, { id, focus }: { id: string | null, focus: boolean }, template: IExtensionEditorTemplate): void { if (this.editorLoadComplete) { /* __GDPR__ "extensionEditor:navbarChange" : { @@ -509,30 +537,32 @@ export class ExtensionEditor extends BaseEditor { } this.contentDisposables.clear(); - this.content.innerHTML = ''; + template.content.innerHTML = ''; this.activeElement = null; - this.open(id, extension) - .then(activeElement => { - this.activeElement = activeElement; - if (focus) { - this.focus(); - } - }); + if (id) { + this.open(id, extension, template) + .then(activeElement => { + this.activeElement = activeElement; + if (focus) { + this.focus(); + } + }); + } } - private open(id: string, extension: IExtension): Promise { + private open(id: string, extension: IExtension, template: IExtensionEditorTemplate): Promise { switch (id) { - case NavbarSection.Readme: return this.openReadme(); - case NavbarSection.Contributions: return this.openContributions(); - case NavbarSection.Changelog: return this.openChangelog(); - case NavbarSection.Dependencies: return this.openDependencies(extension); - case NavbarSection.ExtensionPack: return this.openExtensionPack(extension); + case NavbarSection.Readme: return this.openReadme(template); + case NavbarSection.Contributions: return this.openContributions(template); + case NavbarSection.Changelog: return this.openChangelog(template); + case NavbarSection.Dependencies: return this.openDependencies(extension, template); + case NavbarSection.ExtensionPack: return this.openExtensionPack(extension, template); } return Promise.resolve(null); } - private openMarkdown(cacheResult: CacheResult, noContentCopy: string): Promise { - return this.loadContents(() => cacheResult) + private openMarkdown(cacheResult: CacheResult, noContentCopy: string, template: IExtensionEditorTemplate): Promise { + return this.loadContents(() => cacheResult, template) .then(marked.parse) .then(content => this.renderBody(content)) .then(removeEmbeddedSVGs) @@ -544,7 +574,7 @@ export class ExtensionEditor extends BaseEditor { { svgWhiteList: this.extensionsWorkbenchService.allowedBadgeProviders, }); - webviewElement.mountTo(this.content); + webviewElement.mountTo(template.content); this.contentDisposables.add(webviewElement.onDidFocus(() => this.fireOnDidFocus())); const removeLayoutParticipant = arrays.insert(this.layoutParticipants, webviewElement); this.contentDisposables.add(toDisposable(removeLayoutParticipant)); @@ -563,7 +593,7 @@ export class ExtensionEditor extends BaseEditor { return webviewElement; }) .then(undefined, () => { - const p = append(this.content, $('p.nocontent')); + const p = append(template.content, $('p.nocontent')); p.textContent = noContentCopy; return p; }); @@ -757,17 +787,17 @@ export class ExtensionEditor extends BaseEditor { `; } - private openReadme(): Promise { - return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available.")); + private openReadme(template: IExtensionEditorTemplate): Promise { + return this.openMarkdown(this.extensionReadme!.get(), localize('noReadme', "No README available."), template); } - private openChangelog(): Promise { - return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available.")); + private openChangelog(template: IExtensionEditorTemplate): Promise { + return this.openMarkdown(this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template); } - private openContributions(): Promise { + private openContributions(template: IExtensionEditorTemplate): Promise { const content = $('div', { class: 'subcontent', tabindex: '0' }); - return this.loadContents(() => this.extensionManifest!.get()) + return this.loadContents(() => this.extensionManifest!.get(), template) .then(manifest => { if (!manifest) { return content; @@ -798,28 +828,28 @@ export class ExtensionEditor extends BaseEditor { const isEmpty = !renders.some(x => x); if (isEmpty) { append(content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); - append(this.content, content); + append(template.content, content); } else { - append(this.content, scrollableContent.getDomNode()); + append(template.content, scrollableContent.getDomNode()); this.contentDisposables.add(scrollableContent); } return content; }, () => { append(content, $('p.nocontent')).textContent = localize('noContributions', "No Contributions"); - append(this.content, content); + append(template.content, content); return content; }); } - private openDependencies(extension: IExtension): Promise { + private openDependencies(extension: IExtension, template: IExtensionEditorTemplate): Promise { if (arrays.isFalsyOrEmpty(extension.dependencies)) { - append(this.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies"); - return Promise.resolve(this.content); + append(template.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies"); + return Promise.resolve(template.content); } const content = $('div', { class: 'subcontent' }); const scrollableContent = new DomScrollableElement(content, {}); - append(this.content, scrollableContent.getDomNode()); + append(template.content, scrollableContent.getDomNode()); this.contentDisposables.add(scrollableContent); const dependenciesTree = this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension, null, extension => extension.dependencies || [], this.extensionsWorkbenchService), content); @@ -836,10 +866,10 @@ export class ExtensionEditor extends BaseEditor { return Promise.resolve({ focus() { dependenciesTree.domFocus(); } }); } - private openExtensionPack(extension: IExtension): Promise { + private openExtensionPack(extension: IExtension, template: IExtensionEditorTemplate): Promise { const content = $('div', { class: 'subcontent' }); const scrollableContent = new DomScrollableElement(content, {}); - append(this.content, scrollableContent.getDomNode()); + append(template.content, scrollableContent.getDomNode()); this.contentDisposables.add(scrollableContent); const extensionsPackTree = this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension, null, extension => extension.extensionPack || [], this.extensionsWorkbenchService), content); @@ -1259,11 +1289,11 @@ export class ExtensionEditor extends BaseEditor { return null; } - private loadContents(loadingTask: () => CacheResult): Promise { - addClass(this.content, 'loading'); + private loadContents(loadingTask: () => CacheResult, template: IExtensionEditorTemplate): Promise { + addClass(template.content, 'loading'); const result = loadingTask(); - const onDone = () => removeClass(this.content, 'loading'); + const onDone = () => removeClass(template.content, 'loading'); result.promise.then(onDone, onDone); this.contentDisposables.add(toDisposable(() => result.dispose())); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index b598b94ea4b..05bef903d9e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -76,10 +76,10 @@ export function toExtensionDescription(local: ILocalExtension): IExtensionDescri const promptDownloadManually = (extension: IGalleryExtension | undefined, message: string, error: Error, instantiationService: IInstantiationService, notificationService: INotificationService, openerService: IOpenerService, productService: IProductService) => { - if (!extension || error.name === INSTALL_ERROR_INCOMPATIBLE || error.name === INSTALL_ERROR_MALICIOUS || !productService.extensionsGallery) { + if (!extension || error.name === INSTALL_ERROR_INCOMPATIBLE || error.name === INSTALL_ERROR_MALICIOUS || !productService.productConfiguration.extensionsGallery) { return Promise.reject(error); } else { - const downloadUrl = `${productService.extensionsGallery.serviceUrl}/publishers/${extension.publisher}/vsextensions/${extension.name}/${extension.version}/vspackage`; + const downloadUrl = `${productService.productConfiguration.extensionsGallery.serviceUrl}/publishers/${extension.publisher}/vsextensions/${extension.name}/${extension.version}/vspackage`; notificationService.prompt(Severity.Error, message, [{ label: localize('download', "Download Manually"), run: () => openerService.open(URI.parse(downloadUrl)).then(() => { @@ -3022,7 +3022,10 @@ interface IExtensionPickItem extends IQuickPickItem { export class InstallLocalExtensionsInRemoteAction extends Action { + private extensions: IExtension[] | undefined = undefined; + constructor( + private readonly selectAndInstall: boolean, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @@ -3034,32 +3037,56 @@ export class InstallLocalExtensionsInRemoteAction extends Action { ) { super('workbench.extensions.actions.installLocalExtensionsInRemote'); this.update(); - this._register(this.extensionsWorkbenchService.onChange(() => this.update())); + this.extensionsWorkbenchService.queryLocal().then(() => this.updateExtensions()); + this._register(this.extensionsWorkbenchService.onChange(() => { + if (this.extensions) { + this.updateExtensions(); + } + })); } get label(): string { - return this.extensionManagementServerService.remoteExtensionManagementServer ? - localize('install local extensions', "Install Local Extensions in {0}...", this.extensionManagementServerService.remoteExtensionManagementServer.label) : ''; + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + return this.selectAndInstall ? + localize('select and install local extensions', "Install Local Extensions in {0}...", this.extensionManagementServerService.remoteExtensionManagementServer.label) + : localize('install local extensions', "Install Local Extensions in {0}", this.extensionManagementServerService.remoteExtensionManagementServer.label); + } + return ''; + } + + private updateExtensions(): void { + this.extensions = this.extensionsWorkbenchService.local; + this.update(); } private update(): void { - this.enabled = this.getLocalExtensionsToInstall().length > 0; + this.enabled = !!this.extensions && this.getExtensionsToInstall(this.extensions).length > 0; + this.tooltip = this.label; } - private getLocalExtensionsToInstall(): IExtension[] { - return this.extensionsWorkbenchService.local.filter(extension => { + async run(): Promise { + if (this.selectAndInstall) { + return this.selectAndInstallLocalExtensions(); + } else { + const extensionsToInstall = await this.queryExtensionsToInstall(); + return this.installLocalExtensions(extensionsToInstall); + } + } + + private async queryExtensionsToInstall(): Promise { + const local = await this.extensionsWorkbenchService.queryLocal(); + return this.getExtensionsToInstall(local); + } + + private getExtensionsToInstall(local: IExtension[]): IExtension[] { + return local.filter(extension => { const action = this.instantiationService.createInstance(RemoteInstallAction); action.extension = extension; return action.enabled; }); } - async run(): Promise { - this.selectAndInstallLocalExtensions(); - return Promise.resolve(); - } - - private selectAndInstallLocalExtensions(): void { + private async selectAndInstallLocalExtensions(): Promise { const quickPick = this.quickInputService.createQuickPick(); quickPick.busy = true; const disposable = quickPick.onDidAccept(() => { @@ -3069,7 +3096,7 @@ export class InstallLocalExtensionsInRemoteAction extends Action { this.onDidAccept(quickPick.selectedItems); }); quickPick.show(); - const localExtensionsToInstall = this.getLocalExtensionsToInstall(); + const localExtensionsToInstall = await this.queryExtensionsToInstall(); quickPick.busy = false; if (localExtensionsToInstall.length) { quickPick.title = localize('install local extensions title', "Install Local Extensions in {0}", this.extensionManagementServerService.remoteExtensionManagementServer!.label); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 8747ad8ca51..aeb50c75c64 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -156,7 +156,7 @@ export class UnknownExtensionRenderer implements IListRenderer { - return this.extensionsWorkdbenchService.open(this.extensionData.extension, sideByside); + if (this._extensionData) { + return this.extensionsWorkdbenchService.open(this._extensionData.extension, sideByside); + } + return Promise.resolve(); } } @@ -263,4 +262,4 @@ export class ExtensionData implements IExtensionData { } return null; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 50e67a2a888..d9aef35dae5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -22,7 +22,7 @@ import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, AutoUpdate import { ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, - EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction, InstallLocalExtensionsInRemoteAction + EnableAutoUpdateAction, DisableAutoUpdateAction, ShowBuiltInExtensionsAction, InstallVSIXAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -319,7 +319,8 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensionsViewlet { - private onSearchChange: EventOf; + private readonly _onSearchChange: Emitter = this._register(new Emitter()); + private readonly onSearchChange: EventOf = this._onSearchChange.event; private nonEmptyWorkspaceContextKey: IContextKey; private defaultViewsContextKey: IContextKey; private searchMarketplaceExtensionsContextKey: IContextKey; @@ -333,12 +334,10 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private defaultRecommendedExtensionsContextKey: IContextKey; private searchDelayer: Delayer; - private root: HTMLElement; - - private searchBox: SuggestEnabledInput; - private extensionsBox: HTMLElement; - private primaryActions: IAction[]; - private secondaryActions: IAction[] | null; + private root: HTMLElement | undefined; + private searchBox: SuggestEnabledInput | undefined; + private primaryActions: IAction[] | undefined; + private secondaryActions: IAction[] | null = null; private readonly searchViewletState: MementoObject; constructor( @@ -348,7 +347,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio @IInstantiationService instantiationService: IInstantiationService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @IThemeService themeService: IThemeService, @@ -418,32 +416,35 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this._register(attachSuggestEnabledInputBoxStyler(this.searchBox, this.themeService)); - const _searchChange = new Emitter(); - this.onSearchChange = _searchChange.event; this._register(this.searchBox.onInputDidChange(() => { this.triggerSearch(); - _searchChange.fire(this.searchBox.getValue()); + this._onSearchChange.fire(this.searchBox!.getValue()); }, this)); this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this)); this._register(this.onDidChangeVisibility(visible => { if (visible) { - this.searchBox.focus(); + this.searchBox!.focus(); } })); - this.extensionsBox = append(this.root, $('.extensions')); - super.create(this.extensionsBox); + super.create(append(this.root, $('.extensions'))); } focus(): void { - this.searchBox.focus(); + if (this.searchBox) { + this.searchBox.focus(); + } } layout(dimension: Dimension): void { - toggleClass(this.root, 'narrow', dimension.width <= 300); - this.searchBox.layout({ height: 20, width: dimension.width - 34 }); + if (this.root) { + toggleClass(this.root, 'narrow', dimension.width <= 300); + } + if (this.searchBox) { + this.searchBox.layout({ height: 20, width: dimension.width - 34 }); + } super.layout(new Dimension(dimension.width, dimension.height - 38)); } @@ -454,7 +455,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio getActions(): IAction[] { if (!this.primaryActions) { this.primaryActions = [ - this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox.getValue()) + this.instantiationService.createInstance(ClearExtensionsInputAction, ClearExtensionsInputAction.ID, ClearExtensionsInputAction.LABEL, this.onSearchChange, this.searchBox ? this.searchBox.getValue() : '') ]; } return this.primaryActions; @@ -478,7 +479,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.instantiationService.createInstance(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL), ...(this.configurationService.getValue(AutoUpdateConfigurationKey) ? [this.instantiationService.createInstance(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL)] : [this.instantiationService.createInstance(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL), this.instantiationService.createInstance(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL)]), this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL), - ...(this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer ? [this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction)] : []), new Separator(), this.instantiationService.createInstance(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL), this.instantiationService.createInstance(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL) @@ -489,22 +489,24 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio } search(value: string): void { - const event = new Event('input', { bubbles: true }) as SearchInputEvent; - event.immediate = true; + if (this.searchBox) { + const event = new Event('input', { bubbles: true }) as SearchInputEvent; + event.immediate = true; - this.searchBox.setValue(value); + this.searchBox.setValue(value); + } } - private triggerSearch(immediate = false): void { - this.searchDelayer.trigger(() => this.doSearch(), immediate || !this.searchBox.getValue() ? 0 : 500).then(undefined, err => this.onError(err)); + private triggerSearch(): void { + this.searchDelayer.trigger(() => this.doSearch(), this.searchBox && this.searchBox.getValue() ? 500 : 0).then(undefined, err => this.onError(err)); } private normalizedQuery(): string { - return this.searchBox.getValue().replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:'); + return this.searchBox ? this.searchBox.getValue().replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:') : ''; } protected saveState(): void { - const value = this.searchBox.getValue(); + const value = this.searchBox ? this.searchBox.getValue() : ''; if (ExtensionsListView.isLocalExtensionsQuery(value)) { this.searchViewletState['query.value'] = value; } else { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index ae585737ab7..733aa5fa5b1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -14,7 +14,7 @@ import { IExtensionManagementServer, IExtensionManagementServerService, IExtensi import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { append, $, toggleClass } from 'vs/base/browser/dom'; +import { append, $, toggleClass, addClass } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer, IExtensionsViewState } from 'vs/workbench/contrib/extensions/browser/extensionsList'; import { IExtension, IExtensionsWorkbenchService, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -27,8 +27,8 @@ import { OpenGlobalSettingsAction } from 'vs/workbench/contrib/preferences/brows import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -73,15 +73,16 @@ class ExtensionListViewWarning extends Error { } export class ExtensionsListView extends ViewletPanel { - private readonly server: IExtensionManagementServer | undefined; - private messageContainer: HTMLElement; - private messageSeverityIcon: HTMLElement; - private messageBox: HTMLElement; - private extensionsList: HTMLElement; - private badge: CountBadge; - protected badgeContainer: HTMLElement; - private list: WorkbenchPagedList | null; - private queryRequest: { query: string, request: CancelablePromise> } | null; + protected readonly server: IExtensionManagementServer | undefined; + private bodyTemplate: { + messageContainer: HTMLElement; + messageSeverityIcon: HTMLElement; + messageBox: HTMLElement; + extensionsList: HTMLElement; + } | undefined; + private badge: CountBadge | undefined; + private list: WorkbenchPagedList | null = null; + private queryRequest: { query: string, request: CancelablePromise> } | null = null; constructor( options: ExtensionsListViewOptions, @@ -103,31 +104,27 @@ export class ExtensionsListView extends ViewletPanel { @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService); this.server = options.server; } protected renderHeader(container: HTMLElement): void { - this.renderHeaderTitle(container); - } + addClass(container, 'extension-view-header'); + super.renderHeader(container); - renderHeaderTitle(container: HTMLElement): void { - super.renderHeaderTitle(container, this.title); - - this.badgeContainer = append(container, $('.count-badge-wrapper')); - this.badge = new CountBadge(this.badgeContainer); + this.badge = new CountBadge(append(container, $('.count-badge-wrapper'))); this._register(attachBadgeStyler(this.badge, this.themeService)); } renderBody(container: HTMLElement): void { - this.extensionsList = append(container, $('.extensions-list')); - this.messageContainer = append(container, $('.message-container')); - this.messageSeverityIcon = append(this.messageContainer, $('')); - this.messageBox = append(this.messageContainer, $('.message')); + const extensionsList = append(container, $('.extensions-list')); + const messageContainer = append(container, $('.message-container')); + const messageSeverityIcon = append(messageContainer, $('')); + const messageBox = append(messageContainer, $('.message')); const delegate = new Delegate(); const extensionsViewState = new ExtensionsViewState(); const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState); - this.list = this.instantiationService.createInstance(WorkbenchPagedList, this.extensionsList, delegate, [renderer], { + this.list = this.instantiationService.createInstance(WorkbenchPagedList, extensionsList, delegate, [renderer], { ariaLabel: localize('extensions', "Extensions"), multipleSelectionSupport: false, setRowLineHeight: false, @@ -147,10 +144,19 @@ export class ExtensionsListView extends ViewletPanel { .map(e => e.elements[0]) .filter(e => !!e) .on(this.pin, this)); + + this.bodyTemplate = { + extensionsList, + messageBox, + messageContainer, + messageSeverityIcon + }; } protected layoutBody(height: number, width: number): void { - this.extensionsList.style.height = height + 'px'; + if (this.bodyTemplate) { + this.bodyTemplate.extensionsList.style.height = height + 'px'; + } if (this.list) { this.list.layout(height, width); } @@ -482,7 +488,7 @@ export class ExtensionsListView extends ViewletPanel { } - private _searchExperiments: Promise; + private _searchExperiments: Promise | undefined; private getSearchExperiments(): Promise { if (!this._searchExperiments) { this._searchExperiments = this.experimentService.getExperimentsByType(ExperimentActionType.ExtensionSearchResults); @@ -693,24 +699,27 @@ export class ExtensionsListView extends ViewletPanel { this.list.scrollTop = 0; const count = this.count(); - toggleClass(this.extensionsList, 'hidden', count === 0); - toggleClass(this.messageContainer, 'hidden', count > 0); - this.badge.setCount(count); + if (this.bodyTemplate && this.badge) { - if (count === 0 && this.isBodyVisible()) { - if (error) { - if (error instanceof ExtensionListViewWarning) { - this.messageSeverityIcon.className = SeverityIcon.className(Severity.Warning); - this.messageBox.textContent = getErrorMessage(error); + toggleClass(this.bodyTemplate.extensionsList, 'hidden', count === 0); + toggleClass(this.bodyTemplate.messageContainer, 'hidden', count > 0); + this.badge.setCount(count); + + if (count === 0 && this.isBodyVisible()) { + if (error) { + if (error instanceof ExtensionListViewWarning) { + this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Warning); + this.bodyTemplate.messageBox.textContent = getErrorMessage(error); + } else { + this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(Severity.Error); + this.bodyTemplate.messageBox.textContent = localize('error', "Error while loading extensions. {0}", getErrorMessage(error)); + } } else { - this.messageSeverityIcon.className = SeverityIcon.className(Severity.Error); - this.messageBox.textContent = localize('error', "Error while loading extensions. {0}", getErrorMessage(error)); + this.bodyTemplate.messageSeverityIcon.className = ''; + this.bodyTemplate.messageBox.textContent = localize('no extensions found', "No extensions found."); } - } else { - this.messageSeverityIcon.className = ''; - this.messageBox.textContent = localize('no extensions found', "No extensions found."); + alert(this.bodyTemplate.messageBox.textContent); } - alert(this.messageBox.textContent); } } } @@ -850,7 +859,7 @@ export class ServerExtensionsView extends ExtensionsListView { @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService ) { options.server = server; super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService); @@ -864,6 +873,15 @@ export class ServerExtensionsView extends ExtensionsListView { } return super.show(query.trim()); } + + getActions(): IAction[] { + if (this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === this.server) { + const installLocalExtensionsInRemoteAction = this._register(this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction, false)); + installLocalExtensionsInRemoteAction.class = 'octicon octicon-cloud-download'; + return [installLocalExtensionsInRemoteAction]; + } + return []; + } } export class EnabledExtensionsView extends ExtensionsListView { @@ -943,7 +961,7 @@ export class RecommendedExtensionsView extends ExtensionsListView { export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { private readonly recommendedExtensionsQuery = '@recommended:workspace'; - private installAllAction: InstallWorkspaceRecommendedExtensionsAction; + private installAllAction: InstallWorkspaceRecommendedExtensionsAction | undefined; renderBody(container: HTMLElement): void { super.renderBody(container); @@ -953,25 +971,15 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { this._register(this.contextService.onDidChangeWorkbenchState(() => this.update())); } - renderHeader(container: HTMLElement): void { - super.renderHeader(container); + getActions(): IAction[] { + if (!this.installAllAction) { + this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL, [])); + this.installAllAction.class = 'octicon octicon-cloud-download'; + } - const listActionBar = $('.list-actionbar-container'); - container.insertBefore(listActionBar, this.badgeContainer); - - const actionbar = this._register(new ActionBar(listActionBar, { - animated: false - })); - actionbar.onDidRun(({ error }) => error && this.notificationService.error(error)); - - this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL, [])); const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)); - - this.installAllAction.class = 'octicon octicon-cloud-download'; configureWorkspaceFolderAction.class = 'octicon octicon-pencil'; - - actionbar.push([this.installAllAction], { icon: true, label: false }); - actionbar.push([configureWorkspaceFolderAction], { icon: true, label: false }); + return [this.installAllAction, configureWorkspaceFolderAction]; } async show(query: string): Promise> { @@ -986,9 +994,11 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { this.setRecommendationsToInstall(); } - private setRecommendationsToInstall(): Promise { - return this.getRecommendationsToInstall() - .then(recommendations => { this.installAllAction.recommendations = recommendations; }); + private async setRecommendationsToInstall(): Promise { + const recommendations = await this.getRecommendationsToInstall(); + if (this.installAllAction) { + this.installAllAction.recommendations = recommendations; + } } private getRecommendationsToInstall(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index e27cc72e7d1..05741f72a62 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -18,9 +18,9 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { - private _extension: IExtension; - get extension(): IExtension { return this._extension; } - set extension(extension: IExtension) { this._extension = extension; this.update(); } + private _extension: IExtension | null = null; + get extension(): IExtension | null { return this._extension; } + set extension(extension: IExtension | null) { this._extension = extension; this.update(); } update(): void { this.render(); } abstract render(): void; } @@ -183,7 +183,7 @@ export class RecommendationWidget extends ExtensionWidget { private element?: HTMLElement; private readonly disposables = this._register(new DisposableStore()); - private _tooltip: string; + private _tooltip: string = ''; get tooltip(): string { return this._tooltip; } set tooltip(tooltip: string) { if (this._tooltip !== tooltip) { @@ -314,4 +314,4 @@ class RemoteBadge extends Disposable { updateTitle(); } } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 7ddf7c7fa70..99ec538b442 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -114,11 +114,11 @@ class Extension implements IExtension { } get url(): string | undefined { - if (!this.productService.extensionsGallery || !this.gallery) { + if (!this.productService.productConfiguration.extensionsGallery || !this.gallery) { return undefined; } - return `${this.productService.extensionsGallery.itemUrl}?itemName=${this.publisher}.${this.name}`; + return `${this.productService.productConfiguration.extensionsGallery.itemUrl}?itemName=${this.publisher}.${this.name}`; } get iconUrl(): string { @@ -484,7 +484,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private readonly _onChange: Emitter = new Emitter(); get onChange(): Event { return this._onChange.event; } - private _extensionAllowedBadgeProviders: string[]; + private _extensionAllowedBadgeProviders: string[] | undefined; private installing: IExtension[] = []; constructor( @@ -615,7 +615,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension text = text.replace(extensionRegex, (m, ext) => { // Get curated keywords - const lookup = this.productService.extensionKeywords || {}; + const lookup = this.productService.productConfiguration.extensionKeywords || {}; const keywords = lookup[ext] || []; // Get mode name @@ -1022,7 +1022,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension get allowedBadgeProviders(): string[] { if (!this._extensionAllowedBadgeProviders) { - this._extensionAllowedBadgeProviders = (this.productService.extensionAllowedBadgeProviders || []).map(s => s.toLowerCase()); + this._extensionAllowedBadgeProviders = (this.productService.productConfiguration.extensionAllowedBadgeProviders || []).map(s => s.toLowerCase()); } return this._extensionAllowedBadgeProviders; } @@ -1096,12 +1096,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } - private _ignoredAutoUpdateExtensions: string[]; + private _ignoredAutoUpdateExtensions: string[] | undefined; private get ignoredAutoUpdateExtensions(): string[] { if (!this._ignoredAutoUpdateExtensions) { this._ignoredAutoUpdateExtensions = JSON.parse(this.storageService.get('extensions.ignoredAutoUpdateExtension', StorageScope.GLOBAL, '[]') || '[]'); } - return this._ignoredAutoUpdateExtensions; + return this._ignoredAutoUpdateExtensions!; } private set ignoredAutoUpdateExtensions(extensionIds: string[]) { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensions.css b/src/vs/workbench/contrib/extensions/browser/media/extensions.css index 4a74b3ce38d..ad1858f6f05 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensions.css @@ -6,8 +6,3 @@ .monaco-workbench .activitybar > .content .monaco-action-bar .action-label.extensions { -webkit-mask: url('extensions-activity-bar.svg') no-repeat 50% 50%; } - -.extensions .split-view-view .panel-header .count-badge-wrapper { - position: absolute; - right: 12px; -} \ No newline at end of file diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index ede6bff2a64..57be7e4e1d4 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -27,13 +27,16 @@ height: calc(100% - 38px); } -.extensions-viewlet > .extensions .list-actionbar-container .monaco-action-bar .action-item > .octicon { - font-size: 12px; - line-height: 1; - margin-right: 10px; +.extensions-viewlet > .extensions .extension-view-header .monaco-action-bar { + margin-right: 4px; } -.extensions-viewlet > .extensions .list-actionbar-container .monaco-action-bar .action-item.disabled { +.extensions-viewlet > .extensions .extension-view-header .monaco-action-bar .action-item > .action-label.icon.octicon { + vertical-align: middle; + line-height: 22px; +} + +.extensions-viewlet > .extensions .extension-view-header .monaco-action-bar .action-item.disabled { display: none; } @@ -44,7 +47,7 @@ } .extensions-viewlet > .extensions .panel-header { - padding-right: 28px; + padding-right: 6px; } .extensions-viewlet > .extensions .panel-header > .title { diff --git a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts index 84c47be3064..b140853a820 100644 --- a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts +++ b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts @@ -22,7 +22,7 @@ export class RemoteExtensionsInstaller extends Disposable implements IWorkbenchC ) { super(); if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { - const installLocalExtensionsInRemoteAction = instantiationService.createInstance(InstallLocalExtensionsInRemoteAction); + const installLocalExtensionsInRemoteAction = instantiationService.createInstance(InstallLocalExtensionsInRemoteAction, true); CommandsRegistry.registerCommand('workbench.extensions.installLocalExtensions', () => installLocalExtensionsInRemoteAction.run()); let disposable = Disposable.None; const appendMenuItem = () => { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts index b424211b797..5610f403056 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts @@ -14,7 +14,6 @@ import { IExtensionTipsService, ExtensionRecommendationReason, IExtensionsConfig import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextModel } from 'vs/editor/common/model'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import product from 'vs/platform/product/node/product'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import Severity from 'vs/base/common/severity'; @@ -23,13 +22,11 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IExtensionsConfiguration, ConfigurationKey, ShowRecommendationsOnlyOnDemandKey, IExtensionsViewlet, IExtensionsWorkbenchService, EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import * as pfs from 'vs/base/node/pfs'; import * as os from 'os'; import { flatten, distinct, shuffle, coalesce } from 'vs/base/common/arrays'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { guessMimeTypes, MIME_UNKNOWN } from 'vs/base/common/mime'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { getHashedRemotesFromUri } from 'vs/workbench/contrib/stats/electron-browser/workspaceStats'; import { IRequestService, asJson } from 'vs/platform/request/common/request'; import { isNumber } from 'vs/base/common/types'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -42,9 +39,9 @@ import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/wo import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { extname } from 'vs/base/common/resources'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IExeBasedExtensionTip } from 'vs/platform/product/common/product'; +import { IExeBasedExtensionTip, IProductService } from 'vs/platform/product/common/product'; import { timeout } from 'vs/base/common/async'; +import { IWorkspaceStatsService } from 'vs/workbench/contrib/stats/common/workspaceStats'; const milliSecondsInADay = 1000 * 60 * 60 * 24; const choiceNever = localize('neverShowAgain', "Don't Show Again"); @@ -109,7 +106,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, @IExperimentService private readonly experimentService: IExperimentService, - @ITextFileService private readonly textFileService: ITextFileService + @IWorkspaceStatsService private readonly workspaceStatsService: IWorkspaceStatsService, + @IProductService private readonly productService: IProductService ) { super(); @@ -117,8 +115,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return; } - if (product.extensionsGallery && product.extensionsGallery.recommendationsUrl) { - this._extensionsRecommendationsUrl = product.extensionsGallery.recommendationsUrl; + if (this.productService.productConfiguration.extensionsGallery && this.productService.productConfiguration.extensionsGallery.recommendationsUrl) { + this._extensionsRecommendationsUrl = this.productService.productConfiguration.extensionsGallery.recommendationsUrl; } this.sessionSeed = +new Date(); @@ -244,7 +242,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } getKeymapRecommendations(): IExtensionRecommendation[] { - return (product.keymapExtensionTips || []) + return (this.productService.productConfiguration.keymapExtensionTips || []) .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)) .map(extensionId => ({ extensionId, sources: ['application'] })); } @@ -601,10 +599,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return Object.keys(this._fileBasedRecommendations) .sort((a, b) => { if (this._fileBasedRecommendations[a].recommendedTime === this._fileBasedRecommendations[b].recommendedTime) { - if (!product.extensionImportantTips || caseInsensitiveGet(product.extensionImportantTips, a)) { + if (!this.productService.productConfiguration.extensionImportantTips || caseInsensitiveGet(this.productService.productConfiguration.extensionImportantTips, a)) { return -1; } - if (caseInsensitiveGet(product.extensionImportantTips, b)) { + if (caseInsensitiveGet(this.productService.productConfiguration.extensionImportantTips, b)) { return 1; } } @@ -615,11 +613,11 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } /** - * Parse all file based recommendations from product.extensionTips - * Retire existing recommendations if they are older than a week or are not part of product.extensionTips anymore + * Parse all file based recommendations from this.productService.productConfiguration.extensionTips + * Retire existing recommendations if they are older than a week or are not part of this.productService.productConfiguration.extensionTips anymore */ private fetchFileBasedRecommendations() { - const extensionTips = product.extensionTips; + const extensionTips = this.productService.productConfiguration.extensionTips; if (!extensionTips) { return; } @@ -636,7 +634,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } }); - forEach(product.extensionImportantTips, entry => { + forEach(this.productService.productConfiguration.extensionImportantTips, entry => { let { key: id, value } = entry; const { pattern } = value; let ids = this._availableRecommendations[pattern]; @@ -698,7 +696,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe let { key: pattern, value: ids } = entry; if (match(pattern, model.uri.toString())) { for (let id of ids) { - if (caseInsensitiveGet(product.extensionImportantTips, id)) { + if (caseInsensitiveGet(this.productService.productConfiguration.extensionImportantTips, id)) { recommendationsToSuggest.push(id); } const filedBasedRecommendation = this._fileBasedRecommendations[id.toLowerCase()] || { recommendedTime: now, sources: [] }; @@ -752,7 +750,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } const id = recommendationsToSuggest[0]; - const entry = caseInsensitiveGet(product.extensionImportantTips, id); + const entry = caseInsensitiveGet(this.productService.productConfiguration.extensionImportantTips, id); if (!entry) { return false; } @@ -982,14 +980,14 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } /** - * If user has any of the tools listed in product.exeBasedExtensionTips, fetch corresponding recommendations + * If user has any of the tools listed in this.productService.productConfiguration.exeBasedExtensionTips, fetch corresponding recommendations */ private fetchExecutableRecommendations(important: boolean): Promise { const homeDir = os.homedir(); let foundExecutables: Set = new Set(); let findExecutable = (exeName: string, tip: IExeBasedExtensionTip, path: string) => { - return pfs.fileExists(path).then(exists => { + return this.fileService.exists(URI.file(path)).then(exists => { if (exists && !foundExecutables.has(exeName)) { foundExecutables.add(exeName); (tip['recommendations'] || []).forEach(extensionId => { @@ -1006,7 +1004,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe let promises: Promise[] = []; // Loop through recommended extensions - forEach(product.exeBasedExtensionTips, entry => { + forEach(this.productService.productConfiguration.exeBasedExtensionTips, entry => { if (typeof entry.value !== 'object' || !Array.isArray(entry.value['recommendations'])) { return; } @@ -1078,7 +1076,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe const storageKey = 'extensionsAssistant/dynamicWorkspaceRecommendations'; const workspaceUri = this.contextService.getWorkspace().folders[0].uri; - return Promise.all([getHashedRemotesFromUri(workspaceUri, this.fileService, this.textFileService, false), getHashedRemotesFromUri(workspaceUri, this.fileService, this.textFileService, true)]).then(([hashedRemotes1, hashedRemotes2]) => { + return Promise.all([this.workspaceStatsService.getHashedRemotesFromUri(workspaceUri, false), this.workspaceStatsService.getHashedRemotesFromUri(workspaceUri, true)]).then(([hashedRemotes1, hashedRemotes2]) => { const hashedRemotes = (hashedRemotes1 || []).concat(hashedRemotes2 || []); if (!hashedRemotes.length) { return undefined; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index cf2b6c79522..a618098b66c 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -35,8 +35,7 @@ import { URLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { SinonStub } from 'sinon'; -import { IExperimentService, ExperimentState, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { ExperimentService } from 'vs/workbench/contrib/experiments/electron-browser/experimentService'; +import { IExperimentService, ExperimentState, ExperimentActionType, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.ts b/src/vs/workbench/contrib/feedback/browser/feedback.ts index 618dc7edaa9..130c7fd3a04 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -73,8 +73,8 @@ export class FeedbackDropdown extends Dropdown { this.feedbackDelegate = options.feedbackService; this.maxFeedbackCharacters = this.feedbackDelegate.getCharacterLimit(this.sentiment); - if (productService.sendASmile) { - this.requestFeatureLink = productService.sendASmile.requestFeatureUrl; + if (productService.productConfiguration.sendASmile) { + this.requestFeatureLink = productService.productConfiguration.sendASmile.requestFeatureUrl; } this.integrityService.isPure().then(result => { diff --git a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts index c609aa4132f..458dc93b0b6 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts @@ -58,7 +58,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben ) { super(); - if (productService.sendASmile) { + if (productService.productConfiguration.sendASmile) { this.entry = this._register(statusbarService.addEntry(this.getStatusEntry(), 'status.feedback', localize('status.feedback', "Tweet Feedback"), StatusbarAlignment.RIGHT, -100 /* towards the end of the right hand side */)); CommandsRegistry.registerCommand('_feedback.open', () => this.toggleFeedback()); diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 580a2990ac4..d6ce1130e86 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -7,16 +7,15 @@ import * as nls from 'vs/nls'; import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { URI } from 'vs/base/common/uri'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; /** * An implementation of editor for binary files like images. @@ -28,7 +27,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IWindowsService private readonly windowsService: IWindowsService, + @IOpenerService private readonly openerService: IOpenerService, @IEditorService private readonly editorService: IEditorService, @IStorageService storageService: IStorageService, @IFileService fileService: IFileService, @@ -39,7 +38,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { BinaryFileEditor.ID, { openInternal: (input, options) => this.openInternal(input, options), - openExternal: resource => this.openExternal(resource) + openExternal: resource => this.openerService.openExternal(resource) }, telemetryService, themeService, @@ -58,13 +57,6 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } } - private async openExternal(resource: URI): Promise { - const didOpen = await this.windowsService.openExternal(resource.toString()); - if (!didOpen) { - return this.windowsService.showItemInFolder(resource); - } - } - getTitle(): string | null { return this.input ? this.input.getName() : nls.localize('binaryFileEditor', "Binary File Viewer"); } diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index 9d0f177a77a..aff4390570c 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -158,7 +158,11 @@ export class ExplorerService implements IExplorerService { // Stat needs to be resolved first and then revealed const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === 'modified' }; const workspaceFolder = this.contextService.getWorkspaceFolder(resource); - const rootUri = workspaceFolder ? workspaceFolder.uri : this.roots[0].resource; + if (workspaceFolder === null) { + return Promise.resolve(undefined); + } + const rootUri = workspaceFolder.uri; + const root = this.roots.filter(r => r.resource.toString() === rootUri.toString()).pop()!; try { diff --git a/src/vs/workbench/contrib/markers/browser/markersPanel.ts b/src/vs/workbench/contrib/markers/browser/markersPanel.ts index 71b13da1413..36f1b9bd365 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersPanel.ts @@ -534,8 +534,10 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { const span1 = dom.append(container, dom.$('span')); span1.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS; const link = dom.append(container, dom.$('a.messageAction')); - link.textContent = localize('clearFilter', "Clear Filter."); + link.textContent = localize('clearFilter', "Clear Filter"); link.setAttribute('tabIndex', '0'); + const span2 = dom.append(container, dom.$('span')); + span2.textContent = '.'; dom.addStandardDisposableListener(link, dom.EventType.CLICK, () => this.filterAction.filterText = ''); dom.addStandardDisposableListener(link, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => { if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 2887abfc0f3..ef36014032d 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -73,7 +73,7 @@ export class PreferencesSearchService extends Disposable implements IPreferences }; } else { return { - urlBase: this.productService.settingsSearchUrl + urlBase: this.productService.productConfiguration.settingsSearchUrl }; } } @@ -364,7 +364,7 @@ class RemoteSearchProvider implements ISearchProvider { const extensions = await this.installedExtensions; const filters = this.options.newExtensionsOnly ? [`diminish eq 'latest'`] : - this.getVersionFilters(extensions, this.productService.settingsSearchBuildId); + this.getVersionFilters(extensions, this.productService.productConfiguration.settingsSearchBuildId); const filterStr = filters .slice(filterPage * RemoteSearchProvider.MAX_REQUEST_FILTERS, (filterPage + 1) * RemoteSearchProvider.MAX_REQUEST_FILTERS) @@ -563,4 +563,4 @@ export class SettingMatches { endColumn: setting.valueRange.startColumn + match.end + 1 }; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 7eeb9c15120..66678cbfffb 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -36,7 +36,7 @@ import { ISettingsGroup } from 'vs/workbench/services/preferences/common/prefere export class SettingsHeaderWidget extends Widget implements IViewZone { - private id: number; + private id: string; private _domNode: HTMLElement; protected titleContainer: HTMLElement; @@ -121,7 +121,7 @@ export class DefaultSettingsHeaderWidget extends SettingsHeaderWidget { export class SettingsGroupTitleWidget extends Widget implements IViewZone { - private id: number; + private id: string; private _afterLineNumber: number; private _domNode: HTMLElement; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 7106918e292..82465a22624 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; diff --git a/src/vs/workbench/contrib/stats/common/workspaceStats.ts b/src/vs/workbench/contrib/stats/common/workspaceStats.ts new file mode 100644 index 00000000000..7bd69fc83cd --- /dev/null +++ b/src/vs/workbench/contrib/stats/common/workspaceStats.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; + +export type Tags = { [index: string]: boolean | number | string | undefined }; + +export const IWorkspaceStatsService = createDecorator('workspaceStatsService'); + +export interface IWorkspaceStatsService { + _serviceBrand: any; + + getTags(): Promise; + + /** + * Returns an id for the workspace, different from the id returned by the context service. A hash based + * on the folder uri or workspace configuration, not time-based, and undefined for empty workspaces. + */ + getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined; + + getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit?: boolean): Promise; +} diff --git a/src/vs/workbench/contrib/stats/electron-browser/workspaceStats.ts b/src/vs/workbench/contrib/stats/electron-browser/workspaceStats.ts index 1eac917c60c..d6d6c795e0c 100644 --- a/src/vs/workbench/contrib/stats/electron-browser/workspaceStats.ts +++ b/src/vs/workbench/contrib/stats/electron-browser/workspaceStats.ts @@ -14,7 +14,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { endsWith } from 'vs/base/common/strings'; import { ITextFileService, } from 'vs/workbench/services/textfile/common/textfiles'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { IWorkspaceStatsService, Tags } from 'vs/workbench/contrib/stats/electron-browser/workspaceStatsService'; +import { IWorkspaceStatsService, Tags } from 'vs/workbench/contrib/stats/common/workspaceStats'; import { IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnosticsService'; const SshProtocolMatcher = /^([^@:]+@)?([^:]+):/; @@ -136,20 +136,6 @@ export function getHashedRemotesFromConfig(text: string, stripEndingDotGit: bool }); } -export function getHashedRemotesFromUri(workspaceUri: URI, fileService: IFileService, textFileService: ITextFileService, stripEndingDotGit: boolean = false): Promise { - const path = workspaceUri.path; - const uri = workspaceUri.with({ path: `${path !== '/' ? path : ''}/.git/config` }); - return fileService.exists(uri).then(exists => { - if (!exists) { - return []; - } - return textFileService.read(uri, { acceptTextOnly: true }).then( - content => getHashedRemotesFromConfig(content.value, stripEndingDotGit), - err => [] // ignore missing or binary file - ); - }); -} - export class WorkspaceStats implements IWorkbenchContribution { constructor( @@ -230,7 +216,7 @@ export class WorkspaceStats implements IWorkbenchContribution { private reportRemotes(workspaceUris: URI[]): void { Promise.all(workspaceUris.map(workspaceUri => { - return getHashedRemotesFromUri(workspaceUri, this.fileService, this.textFileService, true); + return this.workspaceStatsService.getHashedRemotesFromUri(workspaceUri, true); })).then(hashedRemotes => { /* __GDPR__ "workspace.hashedRemotes" : { diff --git a/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts b/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts index b56c4a821ca..9c1c1d57180 100644 --- a/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts +++ b/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts @@ -18,10 +18,9 @@ import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspa import { localize } from 'vs/nls'; import Severity from 'vs/base/common/severity'; import { joinPath } from 'vs/base/common/resources'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -export type Tags = { [index: string]: boolean | number | string | undefined }; +import { IWorkspaceStatsService, Tags } from 'vs/workbench/contrib/stats/common/workspaceStats'; +import { getHashedRemotesFromConfig } from 'vs/workbench/contrib/stats/electron-browser/workspaceStats'; const DISABLE_WORKSPACE_PROMPT_KEY = 'workspaces.dontPromptToOpen'; @@ -93,20 +92,6 @@ const PyModulesToLookFor = [ 'botframework-connector' ]; -export const IWorkspaceStatsService = createDecorator('workspaceStatsService'); - -export interface IWorkspaceStatsService { - _serviceBrand: any; - getTags(): Promise; - - /** - * Returns an id for the workspace, different from the id returned by the context service. A hash based - * on the folder uri or workspace configuration, not time-based, and undefined for empty workspaces. - */ - getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined; -} - - export class WorkspaceStatsService implements IWorkspaceStatsService { _serviceBrand: any; private _tags: Tags; @@ -152,6 +137,20 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { return workspaceId; } + getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit: boolean = false): Promise { + const path = workspaceUri.path; + const uri = workspaceUri.with({ path: `${path !== '/' ? path : ''}/.git/config` }); + return this.fileService.exists(uri).then(exists => { + if (!exists) { + return []; + } + return this.textFileService.read(uri, { acceptTextOnly: true }).then( + content => getHashedRemotesFromConfig(content.value, stripEndingDotGit), + err => [] // ignore missing or binary file + ); + }); + } + /* __GDPR__FRAGMENT__ "WorkspaceTags" : { "workbench.filesToOpenOrCreate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index dbd0aa41286..2a343f63b05 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -20,7 +20,7 @@ import * as panel from 'vs/workbench/browser/panel'; import { getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { Extensions as QuickOpenExtensions, IQuickOpenRegistry, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; -import { AllowWorkspaceShellTerminalCommand, ClearSelectionTerminalAction, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewInActiveWorkspaceTerminalAction, CreateNewTerminalAction, DeleteToLineStartTerminalAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, DisallowWorkspaceShellTerminalCommand, FindNext, FindPrevious, FocusActiveTerminalAction, FocusNextPaneTerminalAction, FocusNextTerminalAction, FocusPreviousPaneTerminalAction, FocusPreviousTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, KillTerminalAction, MoveToLineEndTerminalAction, MoveToLineStartTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, RenameTerminalAction, ResizePaneDownTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, RunActiveFileInTerminalAction, RunSelectedTextInTerminalAction, ScrollDownPageTerminalAction, ScrollDownTerminalAction, ScrollToBottomTerminalAction, ScrollToNextCommandAction, ScrollToPreviousCommandAction, ScrollToTopTerminalAction, ScrollUpPageTerminalAction, ScrollUpTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SelectToNextCommandAction, SelectToNextLineAction, SelectToPreviousCommandAction, SelectToPreviousLineAction, SendSequenceTerminalCommand, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, TERMINAL_PICKER_PREFIX, ToggleCaseSensitiveCommand, ToggleEscapeSequenceLoggingAction, ToggleRegexCommand, ToggleTerminalAction, ToggleWholeWordCommand, NavigationModeFocusPreviousTerminalAction, NavigationModeFocusNextTerminalAction, NavigationModeExitTerminalAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { ClearSelectionTerminalAction, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewInActiveWorkspaceTerminalAction, CreateNewTerminalAction, DeleteToLineStartTerminalAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, FindNext, FindPrevious, FocusActiveTerminalAction, FocusNextPaneTerminalAction, FocusNextTerminalAction, FocusPreviousPaneTerminalAction, FocusPreviousTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, KillTerminalAction, MoveToLineEndTerminalAction, MoveToLineStartTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, RenameTerminalAction, ResizePaneDownTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, RunActiveFileInTerminalAction, RunSelectedTextInTerminalAction, ScrollDownPageTerminalAction, ScrollDownTerminalAction, ScrollToBottomTerminalAction, ScrollToNextCommandAction, ScrollToPreviousCommandAction, ScrollToTopTerminalAction, ScrollUpPageTerminalAction, ScrollUpTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SelectToNextCommandAction, SelectToNextLineAction, SelectToPreviousCommandAction, SelectToPreviousLineAction, SendSequenceTerminalCommand, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, TERMINAL_PICKER_PREFIX, ToggleCaseSensitiveCommand, ToggleEscapeSequenceLoggingAction, ToggleRegexCommand, ToggleTerminalAction, ToggleWholeWordCommand, NavigationModeFocusPreviousTerminalAction, NavigationModeFocusNextTerminalAction, NavigationModeExitTerminalAction, ManageWorkspaceShellPermissionsTerminalCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalPanel } from 'vs/workbench/contrib/terminal/browser/terminalPanel'; import { TerminalPickerHandler } from 'vs/workbench/contrib/terminal/browser/terminalQuickOpen'; import { KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, ITerminalService, TERMINAL_ACTION_CATEGORY, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -387,8 +387,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ClearTerminalAct mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_K } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectDefaultShellWindowsTerminalAction, SelectDefaultShellWindowsTerminalAction.ID, SelectDefaultShellWindowsTerminalAction.LABEL), 'Terminal: Select Default Shell', category); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(AllowWorkspaceShellTerminalCommand, AllowWorkspaceShellTerminalCommand.ID, AllowWorkspaceShellTerminalCommand.LABEL), 'Terminal: Allow Workspace Shell Configuration', category); -actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(DisallowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand.ID, DisallowWorkspaceShellTerminalCommand.LABEL), 'Terminal: Disallow Workspace Shell Configuration', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ManageWorkspaceShellPermissionsTerminalCommand, ManageWorkspaceShellPermissionsTerminalCommand.ID, ManageWorkspaceShellPermissionsTerminalCommand.LABEL), 'Terminal: Manage Workspace Shell Permissions', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(RenameTerminalAction, RenameTerminalAction.ID, RenameTerminalAction.LABEL), 'Terminal: Rename', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalFindWidgetAction, FocusTerminalFindWidgetAction.ID, FocusTerminalFindWidgetAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_F diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 41ceb3e71b1..4d07f85dc15 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -993,10 +993,10 @@ export class ClearSelectionTerminalAction extends Action { } } -export class AllowWorkspaceShellTerminalCommand extends Action { +export class ManageWorkspaceShellPermissionsTerminalCommand extends Action { - public static readonly ID = TERMINAL_COMMAND_ID.WORKSPACE_SHELL_ALLOW; - public static readonly LABEL = nls.localize('workbench.action.terminal.allowWorkspaceShell', "Allow Workspace Shell Configuration"); + public static readonly ID = TERMINAL_COMMAND_ID.MANAGE_WORKSPACE_SHELL_PERMISSIONS; + public static readonly LABEL = nls.localize('workbench.action.terminal.manageWorkspaceShellPermissions', "Manage Workspace Shell Permissions"); constructor( id: string, label: string, @@ -1005,27 +1005,8 @@ export class AllowWorkspaceShellTerminalCommand extends Action { super(id, label); } - public run(event?: any): Promise { - this.terminalService.setWorkspaceShellAllowed(true); - return Promise.resolve(undefined); - } -} - -export class DisallowWorkspaceShellTerminalCommand extends Action { - - public static readonly ID = TERMINAL_COMMAND_ID.WORKSPACE_SHELL_DISALLOW; - public static readonly LABEL = nls.localize('workbench.action.terminal.disallowWorkspaceShell', "Disallow Workspace Shell Configuration"); - - constructor( - id: string, label: string, - @ITerminalService private readonly terminalService: ITerminalService - ) { - super(id, label); - } - - public run(event?: any): Promise { - this.terminalService.setWorkspaceShellAllowed(false); - return Promise.resolve(undefined); + public async run(event?: any): Promise { + await this.terminalService.manageWorkspaceShellPermissions(); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index b3ff7db5e88..b1ecf1026f3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -226,7 +226,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(); this._configHelper.showRecommendations(shellLaunchConfig); const baseEnv = this._configHelper.config.inheritEnv ? process.env as platform.IProcessEnvironment : await this._terminalInstanceService.getMainProcessParentEnv(); - const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, lastActiveWorkspace, envFromConfigValue, this._configurationResolverService, isWorkspaceShellAllowed, this._productService.version, this._configHelper.config.setLocaleVariables, baseEnv); + const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, lastActiveWorkspace, envFromConfigValue, this._configurationResolverService, isWorkspaceShellAllowed, this._productService.productConfiguration.version, this._configHelper.config.setLocaleVariables, baseEnv); const useConpty = this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled; return this._terminalInstanceService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, useConpty); diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index bb99965b26c..4383fb9c82b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -279,7 +279,7 @@ export interface ITerminalService { selectDefaultWindowsShell(): Promise; setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; - setWorkspaceShellAllowed(isAllowed: boolean): void; + manageWorkspaceShellPermissions(): void; /** * Takes a path and returns the properly escaped path to send to the terminal. diff --git a/src/vs/workbench/contrib/terminal/common/terminalCommands.ts b/src/vs/workbench/contrib/terminal/common/terminalCommands.ts index 60da258805d..ddd108304ae 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalCommands.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalCommands.ts @@ -48,8 +48,7 @@ export const enum TERMINAL_COMMAND_ID { SCROLL_TO_TOP = 'workbench.action.terminal.scrollToTop', CLEAR = 'workbench.action.terminal.clear', CLEAR_SELECTION = 'workbench.action.terminal.clearSelection', - WORKSPACE_SHELL_ALLOW = 'workbench.action.terminal.allowWorkspaceShell', - WORKSPACE_SHELL_DISALLOW = 'workbench.action.terminal.disallowWorkspaceShell', + MANAGE_WORKSPACE_SHELL_PERMISSIONS = 'workbench.action.terminal.manageWorkspaceShellPermissions', RENAME = 'workbench.action.terminal.rename', FIND_WIDGET_FOCUS = 'workbench.action.terminal.focusFindWidget', FIND_WIDGET_HIDE = 'workbench.action.terminal.hideFindWidget', diff --git a/src/vs/workbench/contrib/terminal/common/terminalService.ts b/src/vs/workbench/contrib/terminal/common/terminalService.ts index 1b04048fdcc..d2dc8ec3c59 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalService.ts @@ -479,22 +479,28 @@ export abstract class TerminalService implements ITerminalService { return terminalIndex; } - public setWorkspaceShellAllowed(isAllowed: boolean): void { - this.configHelper.setWorkspaceShellAllowed(isAllowed); + public async manageWorkspaceShellPermissions(): Promise { + const allowItem: IQuickPickItem = { label: nls.localize('workbench.action.terminal.allowWorkspaceShell', "Allow Workspace Shell Configuration") }; + const disallowItem: IQuickPickItem = { label: nls.localize('workbench.action.terminal.disallowWorkspaceShell', "Disallow Workspace Shell Configuration") }; + const value = await this._quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); + if (!value) { + return; + } + this.configHelper.setWorkspaceShellAllowed(value === allowItem); } - protected _showTerminalCloseConfirmation(): Promise { - let message; + protected async _showTerminalCloseConfirmation(): Promise { + let message: string; if (this.terminalInstances.length === 1) { message = nls.localize('terminalService.terminalCloseConfirmationSingular', "There is an active terminal session, do you want to kill it?"); } else { message = nls.localize('terminalService.terminalCloseConfirmationPlural', "There are {0} active terminal sessions, do you want to kill them?", this.terminalInstances.length); } - - return this._dialogService.confirm({ + const res = await this._dialogService.confirm({ message, type: 'warning', - }).then(res => !res.confirmed); + }); + return !res.confirmed; } protected _showNotEnoughSpaceToast(): void { diff --git a/src/vs/workbench/contrib/webview/common/portMapping.ts b/src/vs/workbench/contrib/webview/common/portMapping.ts index 964c0ac6da0..e47390dcf05 100644 --- a/src/vs/workbench/contrib/webview/common/portMapping.ts +++ b/src/vs/workbench/contrib/webview/common/portMapping.ts @@ -47,16 +47,16 @@ export class WebviewPortMappingManager extends Disposable { if (this.extensionLocation && this.extensionLocation.scheme === REMOTE_HOST_SCHEME) { const tunnel = await this.getOrCreateTunnel(mapping.extensionHostPort); if (tunnel) { - return uri.with({ + return encodeURI(uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}`, - }).toString(); + }).toString(true)); } } if (mapping.webviewPort !== mapping.extensionHostPort) { - return uri.with({ + return encodeURI(uri.with({ authority: `${requestLocalHostInfo.address}:${mapping.extensionHostPort}` - }).toString(); + }).toString(true)); } } } @@ -84,4 +84,4 @@ export class WebviewPortMappingManager extends Disposable { } return tunnel; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index a32024e35ac..80e9fe37c3f 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -20,6 +20,8 @@ import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEn import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { WebWorkerExtensionHostStarter } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter'; import { URI } from 'vs/base/common/uri'; +import { isWebExtension } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -34,6 +36,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IFileService fileService: IFileService, @IProductService productService: IProductService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + @IConfigurationService private readonly _configService: IConfigurationService, ) { super( instantiationService, @@ -65,11 +68,14 @@ export class ExtensionService extends AbstractExtensionService implements IExten const remoteAgentConnection = this._remoteAgentService.getConnection()!; - const webHostProcessWorker = this._instantiationService.createInstance(WebWorkerExtensionHostStarter, true, Promise.resolve([]), URI.parse('empty:value')); //todo@joh + const webExtensions = this.getExtensions().then(extensions => extensions.filter(ext => isWebExtension(ext, this._configService))); + const remoteExtensions = this.getExtensions().then(extensions => extensions.filter(ext => !isWebExtension(ext, this._configService))); + + const webHostProcessWorker = this._instantiationService.createInstance(WebWorkerExtensionHostStarter, true, webExtensions, URI.parse('empty:value')); //todo@joh const webHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, webHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents); result.push(webHostProcessManager); - const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, this.getExtensions(), this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); + const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, remoteExtensions, this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents); result.push(remoteExtHostProcessManager); diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts index 1f04364fa5d..7848fdee127 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DefaultWorkerFactory } from 'vs/base/worker/defaultWorkerFactory'; +import { getWorkerBootstrapUrl } from 'vs/base/worker/defaultWorkerFactory'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { VSBuffer } from 'vs/base/common/buffer'; import { createMessageOfType, MessageType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; @@ -49,23 +49,29 @@ export class WebWorkerExtensionHostStarter implements IExtensionHostStarter { if (!this._protocol) { const emitter = new Emitter(); - const worker = new DefaultWorkerFactory('WorkerExtensionHost').create( - 'vs/workbench/services/extensions/worker/extensionHostWorker', data => { - if (data instanceof ArrayBuffer) { - emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); - } else { - console.warn('UNKNOWN data received', data); - this._onDidExit.fire([77, 'UNKNOWN data received']); - } - }, err => { - this._onDidExit.fire([81, err]); - console.error(err); + + const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost'); + const worker = new Worker(url); + + worker.onmessage = (event) => { + const { data } = event; + if (!(data instanceof ArrayBuffer)) { + console.warn('UNKNOWN data received', data); + this._onDidExit.fire([77, 'UNKNOWN data received']); + return; } - ); + + emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); + }; + + worker.onerror = (event) => { + console.error(event.error); + this._onDidExit.fire([81, event.error]); + }; // keep for cleanup this._toDispose.add(emitter); - this._toDispose.add(worker); + this._toDispose.add(toDisposable(() => worker.terminate())); const protocol: IMessagePassingProtocol = { onMessage: emitter.event, @@ -111,15 +117,15 @@ export class WebWorkerExtensionHostStarter implements IExtensionHostStarter { const [telemetryInfo, extensionDescriptions] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._extensions]); const workspace = this._contextService.getWorkspace(); return { - commit: this._productService.commit, - version: this._productService.version, + commit: this._productService.productConfiguration.commit, + version: this._productService.productConfiguration.version, parentPid: -1, environment: { isExtensionDevelopmentDebug: false, appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined, appSettingsHome: this._environmentService.appSettingsHome ? this._environmentService.appSettingsHome : undefined, - appName: this._productService.nameLong, - appUriScheme: this._productService.urlProtocol, + appName: this._productService.productConfiguration.nameLong, + appUriScheme: this._productService.productConfiguration.urlProtocol, appLanguage: platform.language, extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 6445a03a6e6..51f991375a6 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -462,12 +462,12 @@ class ProposedApiController { } this.enableProposedApiForAll = !environmentService.isBuilt || - (!!environmentService.extensionDevelopmentLocationURI && productService.nameLong !== 'Visual Studio Code') || + (!!environmentService.extensionDevelopmentLocationURI && productService.productConfiguration.nameLong !== 'Visual Studio Code') || (this.enableProposedApiFor.length === 0 && 'enable-proposed-api' in environmentService.args); this.productAllowProposedApi = new Set(); - if (isNonEmptyArray(productService.extensionAllowedProposedApi)) { - productService.extensionAllowedProposedApi.forEach((id) => this.productAllowProposedApi.add(ExtensionIdentifier.toKey(id))); + if (isNonEmptyArray(productService.productConfiguration.extensionAllowedProposedApi)) { + productService.productConfiguration.extensionAllowedProposedApi.forEach((id) => this.productAllowProposedApi.add(ExtensionIdentifier.toKey(id))); } } diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index e03ddb10239..2f7d4e01039 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -10,6 +10,11 @@ import { getGalleryExtensionId, areSameExtensions } from 'vs/platform/extensionM import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IProductService } from 'vs/platform/product/common/product'; +export function isWebExtension(manifest: IExtensionManifest, configurationService: IConfigurationService): boolean { + const extensionKind = getExtensionKind(manifest, configurationService); + return extensionKind === 'web'; +} + export function isUIExtension(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean { const uiContributions = ExtensionsRegistry.getExtensionPoints().filter(e => e.defaultExtensionKind !== 'workspace').map(e => e.name); const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); @@ -19,7 +24,7 @@ export function isUIExtension(manifest: IExtensionManifest, productService: IPro case 'workspace': return false; default: { // Tagged as UI extension in product - if (isNonEmptyArray(productService.uiExtensions) && productService.uiExtensions.some(id => areSameExtensions({ id }, { id: extensionId }))) { + if (isNonEmptyArray(productService.productConfiguration.uiExtensions) && productService.productConfiguration.uiExtensions.some(id => areSameExtensions({ id }, { id: extensionId }))) { return true; } // Not an UI extension if it has main diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHostClient.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHostClient.ts index 47aaf4a22bb..ca657398990 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHostClient.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHostClient.ts @@ -71,7 +71,7 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH public start(): Promise { const options: IConnectionOptions = { - commit: this._productService.commit, + commit: this._productService.productConfiguration.commit, socketFactory: this._socketFactory, addressProvider: { getAddress: async () => { @@ -181,15 +181,15 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH const hostExtensions = allExtensions.filter(extension => extension.main && extension.api === 'none').map(extension => extension.identifier); const workspace = this._contextService.getWorkspace(); const r: IInitData = { - commit: this._productService.commit, - version: this._productService.version, + commit: this._productService.productConfiguration.commit, + version: this._productService.productConfiguration.version, parentPid: remoteExtensionHostData.pid, environment: { isExtensionDevelopmentDebug, appRoot: remoteExtensionHostData.appRoot, appSettingsHome: remoteExtensionHostData.appSettingsHome, - appName: this._productService.nameLong, - appUriScheme: this._productService.urlProtocol, + appName: this._productService.productConfiguration.nameLong, + appUriScheme: this._productService.productConfiguration.urlProtocol, appLanguage: platform.language, extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, diff --git a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts b/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts index 84ec145c4c8..75787071f85 100644 --- a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts +++ b/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts @@ -69,7 +69,7 @@ export class RemoteExtensionManagementChannelClient extends ExtensionManagementC const installed = await this.getInstalled(ExtensionType.User); const compatible = await this.galleryService.getCompatibleExtension(extension); if (!compatible) { - return Promise.reject(new Error(localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", extension.identifier.id, this.productService.version))); + return Promise.reject(new Error(localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", extension.identifier.id, this.productService.productConfiguration.version))); } const manifest = await this.galleryService.getManifest(compatible, CancellationToken.None); if (manifest) { @@ -140,4 +140,4 @@ export class RemoteExtensionManagementChannelClient extends ExtensionManagementC } return this.getDependenciesAndPackedExtensionsRecursively(toGet, result, uiExtension, token); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index f20f824a2e2..8abe6aa7f52 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -309,11 +309,6 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { extensionDescription.id = `${extensionDescription.publisher}.${extensionDescription.name}`; extensionDescription.identifier = new ExtensionIdentifier(extensionDescription.id); - // main := absolutePath(`main`) - if (extensionDescription.main) { - extensionDescription.main = path.join(this._absoluteFolderPath, extensionDescription.main); - } - extensionDescription.extensionLocation = URI.file(this._absoluteFolderPath); return extensionDescription; diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 12a6e3ea66a..64c2e0a526e 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -469,26 +469,24 @@ async function readCaCertificates() { } async function readWindowsCaCertificates() { - // Not using await to work around minifier bug (https://github.com/microsoft/vscode/issues/79044). - return import('vscode-windows-ca-certs') - .then(winCA => { - let ders: any[] = []; - const store = winCA(); - try { - let der: any; - while (der = store.next()) { - ders.push(der); - } - } finally { - store.done(); - } + const winCA = await import('vscode-windows-ca-certs'); - const certs = new Set(ders.map(derToPem)); - return { - certs: Array.from(certs), - append: true - }; - }); + let ders: any[] = []; + const store = winCA(); + try { + let der: any; + while (der = store.next()) { + ders.push(der); + } + } finally { + store.done(); + } + + const certs = new Set(ders.map(derToPem)); + return { + certs: Array.from(certs), + append: true + }; } async function readMacCaCertificates() { diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts index 3ed289be397..34e3f35a624 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; @@ -38,21 +37,18 @@ const hostUtil = new class implements IHostUtils { //todo@joh do not allow extensions to call postMessage and other globals... -class ExtensionWorker implements IRequestHandler { - - // worker-contract - readonly _requestHandlerBrand: any; - readonly onmessage: (data: any) => any; +class ExtensionWorker { // protocol readonly protocol: IMessagePassingProtocol; - constructor(postMessage: (message: any, transfer?: Transferable[]) => any) { + constructor() { let emitter = new Emitter(); let terminating = false; - this.onmessage = data => { + onmessage = event => { + const { data } = event; if (!(data instanceof ArrayBuffer)) { console.warn('UNKNOWN data received', data); return; @@ -98,8 +94,8 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise any): IRequestHandler { - const res = new ExtensionWorker(postMessage); +(function create(): void { + const res = new ExtensionWorker(); connectToRenderer(res.protocol).then(data => { @@ -112,6 +108,4 @@ export function create(postMessage: (message: any, transfer?: Transferable[]) => onTerminate = () => extHostMain.terminate(); }); - - return res; -} +})(); diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts new file mode 100644 index 00000000000..79455414c06 --- /dev/null +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +(function () { + + let MonacoEnvironment = (self).MonacoEnvironment; + let monacoBaseUrl = MonacoEnvironment && MonacoEnvironment.baseUrl ? MonacoEnvironment.baseUrl : '../../../../../'; + + if (typeof (self).define !== 'function' || !(self).define.amd) { + importScripts(monacoBaseUrl + 'vs/loader.js'); + } + + require.config({ + baseUrl: monacoBaseUrl, + catchError: true + }); + + require(['vs/workbench/services/extensions/worker/extensionHostWorker'], () => { }, err => console.error(err)); +})(); diff --git a/src/vs/workbench/services/opener/electron-browser/openerService.ts b/src/vs/workbench/services/opener/electron-browser/openerService.ts new file mode 100644 index 00000000000..3085bd78a14 --- /dev/null +++ b/src/vs/workbench/services/opener/electron-browser/openerService.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { OpenerService as BaseOpenerService } from 'vs/editor/browser/services/openerService'; +import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +export class OpenerService extends BaseOpenerService { + + _serviceBrand!: ServiceIdentifier; + + constructor( + @ICodeEditorService codeEditorService: ICodeEditorService, + @ICommandService commandService: ICommandService, + @IWindowsService private readonly windowsService: IWindowsService + ) { + super(codeEditorService, commandService); + } + + async openExternal(resource: URI): Promise { + const success = this.windowsService.openExternal(encodeURI(resource.toString(true))); + if (!success && resource.scheme === Schemas.file) { + await this.windowsService.showItemInFolder(resource); + + return true; + } + + return success; + } +} + +registerSingleton(IOpenerService, OpenerService, true); diff --git a/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts b/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts index fd979ca094c..db4d2038ca3 100644 --- a/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts +++ b/src/vs/workbench/services/remote/browser/remoteAgentServiceImpl.ts @@ -28,7 +28,7 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR super(environmentService); this.socketFactory = new BrowserSocketFactory(webSocketFactory); - this._connection = this._register(new RemoteAgentConnection(environmentService.configuration.remoteAuthority!, productService.commit, this.socketFactory, remoteAuthorityResolverService, signService)); + this._connection = this._register(new RemoteAgentConnection(environmentService.configuration.remoteAuthority!, productService.productConfiguration.commit, this.socketFactory, remoteAuthorityResolverService, signService)); } getConnection(): IRemoteAgentConnection | null { diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index eac9dcfb1d0..2632d935d80 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -79,11 +79,11 @@ export class TelemetryService extends Disposable implements ITelemetryService { ) { super(); - const aiKey = productService.aiConfig && productService.aiConfig.asimovKey; - if (!environmentService.isExtensionDevelopment && !environmentService.args['disable-telemetry'] && !!productService.enableTelemetry && !!aiKey) { + const aiKey = productService.productConfiguration.aiConfig && productService.productConfiguration.aiConfig.asimovKey; + if (!environmentService.isExtensionDevelopment && !environmentService.args['disable-telemetry'] && !!productService.productConfiguration.enableTelemetry && !!aiKey) { const config: ITelemetryServiceConfig = { appender: combinedAppender(new WebTelemetryAppender(aiKey, logService), new LogAppender(logService)), - commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId, environmentService.configuration.remoteAuthority), + commonProperties: resolveWorkbenchCommonProperties(storageService, productService.productConfiguration.commit, productService.productConfiguration.version, environmentService.configuration.machineId, environmentService.configuration.remoteAuthority), piiPaths: [environmentService.appRoot] }; diff --git a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts index 6eaaf220ce5..002b6ae4ce3 100644 --- a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts @@ -34,11 +34,11 @@ export class TelemetryService extends Disposable implements ITelemetryService { ) { super(); - if (!environmentService.isExtensionDevelopment && !environmentService.args['disable-telemetry'] && !!productService.enableTelemetry) { + if (!environmentService.isExtensionDevelopment && !environmentService.args['disable-telemetry'] && !!productService.productConfiguration.enableTelemetry) { const channel = sharedProcessService.getChannel('telemetryAppender'); const config: ITelemetryServiceConfig = { appender: combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService)), - commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId, environmentService.installSourcePath, environmentService.configuration.remoteAuthority), + commonProperties: resolveWorkbenchCommonProperties(storageService, productService.productConfiguration.commit, productService.productConfiguration.version, environmentService.configuration.machineId, environmentService.installSourcePath, environmentService.configuration.remoteAuthority), piiPaths: environmentService.extensionsPath ? [environmentService.appRoot, environmentService.extensionsPath] : [environmentService.appRoot] }; @@ -69,4 +69,4 @@ export class TelemetryService extends Disposable implements ITelemetryService { } } -registerSingleton(ITelemetryService, TelemetryService); \ No newline at end of file +registerSingleton(ITelemetryService, TelemetryService); diff --git a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts index 6bbdf0e42e1..eab223e0d4a 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts @@ -4,16 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; -import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/workbenchTestServices'; import { ITextFileService, snapshotToString, TextFileOperationResult, TextFileOperationError } from 'vs/workbench/services/textfile/common/textfiles'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { Schemas } from 'vs/base/common/network'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { rimraf, RimRafMode, copy, readFile, exists } from 'vs/base/node/pfs'; @@ -36,13 +31,8 @@ import { detectEncodingByBOM } from 'vs/base/test/node/encoding/encoding.test'; class ServiceAccessor { constructor( - @ILifecycleService public lifecycleService: TestLifecycleService, @ITextFileService public textFileService: TestTextFileService, - @IUntitledEditorService public untitledEditorService: IUntitledEditorService, - @IWindowsService public windowsService: TestWindowsService, - @IWorkspaceContextService public contextService: TestContextService, - @IModelService public modelService: ModelServiceImpl, - @IFileService public fileService: TestFileService + @IUntitledEditorService public untitledEditorService: IUntitledEditorService ) { } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 3558c2ffef3..7c12967c4ae 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -80,8 +80,6 @@ import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; -import { OpenerService } from 'vs/editor/browser/services/openerService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { EditorWorkerServiceImpl } from 'vs/editor/common/services/editorWorkerServiceImpl'; import { MarkerDecorationsService } from 'vs/editor/common/services/markerDecorationsServiceImpl'; @@ -102,7 +100,6 @@ import { DownloadService } from 'vs/platform/download/common/downloadService'; registerSingleton(IExtensionGalleryService, ExtensionGalleryService, true); registerSingleton(IContextViewService, ContextViewService, true); registerSingleton(IListService, ListService, true); -registerSingleton(IOpenerService, OpenerService, true); registerSingleton(IEditorWorkerService, EditorWorkerServiceImpl); registerSingleton(IMarkerDecorationsService, MarkerDecorationsService); registerSingleton(IMarkerService, MarkerService, true); @@ -230,4 +227,7 @@ import 'vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution'; // Outline import 'vs/workbench/contrib/outline/browser/outline.contribution'; +// Experiments +import 'vs/workbench/contrib/experiments/browser/experiments.contribution'; + //#endregion diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index c2d6ea7a5bc..d426224accf 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -48,6 +48,7 @@ import 'vs/workbench/services/extensionManagement/node/extensionManagementServic import 'vs/workbench/services/accessibility/node/accessibilityService'; import 'vs/workbench/services/remote/node/tunnelService'; import 'vs/workbench/services/backup/node/backupFileService'; +import 'vs/workbench/services/opener/electron-browser/openerService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -153,9 +154,6 @@ import 'vs/workbench/contrib/themes/test/electron-browser/themes.test.contributi import 'vs/workbench/contrib/welcome/gettingStarted/electron-browser/gettingStarted.contribution'; import 'vs/workbench/contrib/welcome/page/browser/welcomePage.contribution'; -// Experiments -import 'vs/workbench/contrib/experiments/electron-browser/experiments.contribution'; - // Issues import 'vs/workbench/contrib/issue/electron-browser/issue.contribution'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 1a41fc77ac4..169b3c07ef6 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -54,6 +54,8 @@ import { ContextMenuService } from 'vs/platform/contextview/browser/contextMenuS import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService'; import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; +import { OpenerService } from 'vs/editor/browser/services/openerService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; registerSingleton(IRequestService, RequestService, true); registerSingleton(IExtensionManagementService, ExtensionManagementService); @@ -63,6 +65,7 @@ registerSingleton(IClipboardService, BrowserClipboardService, true); registerSingleton(IAccessibilityService, BrowserAccessibilityService, true); registerSingleton(ILifecycleService, BrowserLifecycleService); registerSingleton(IContextMenuService, ContextMenuService); +registerSingleton(IOpenerService, OpenerService, true); //#endregion diff --git a/yarn.lock b/yarn.lock index 0c86a6fc524..dc0fb77b778 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4057,15 +4057,18 @@ gulp-tslint@^8.1.3: plugin-error "1.0.1" through "~2.3.8" -gulp-uglify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.0.tgz#0df0331d72a0d302e3e37e109485dddf33c6d1ca" - integrity sha1-DfAzHXKg0wLj434QlIXd3zPG0co= +gulp-uglify@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.2.tgz#5f5b2e8337f879ca9dec971feb1b82a5a87850b0" + integrity sha512-gk1dhB74AkV2kzqPMQBLA3jPoIAPd/nlNzP2XMDSG8XZrqnlCiDGAqC+rZOumzFvB5zOphlFh6yr3lgcAb/OOg== dependencies: + array-each "^1.0.1" + extend-shallow "^3.0.2" gulplog "^1.0.0" has-gulplog "^0.1.0" - lodash "^4.13.1" + isobject "^3.0.1" make-error-cause "^1.1.1" + safe-buffer "^5.1.2" through2 "^2.0.0" uglify-js "^3.0.5" vinyl-sourcemaps-apply "^0.2.0" @@ -5465,7 +5468,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.13.1, lodash@^4.15.0, lodash@^4.3.0: +lodash@^4.15.0, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4= @@ -9167,15 +9170,7 @@ uc.micro@^1.0.1, uc.micro@^1.0.3: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" integrity sha1-ftUNXg+an7ClczeSWfKndFjVAZI= -uglify-es@^3.0.18: - version "3.1.9" - resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.1.9.tgz#6c82df628ac9eb7af9c61fd70c744a084abe6161" - integrity sha512-wVSiJKHDgDDFmxTVVvnbAH6IpamAFHYDI+5JvwPdaqIMnk8kRTX2JKwq1Fx7gb2+Jj5Dus8kzvIpKkWOMNU51w== - dependencies: - commander "~2.11.0" - source-map "~0.6.1" - -uglify-es@^3.3.4: +uglify-es@^3.3.4, uglify-es@^3.3.9: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" integrity sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==