diff --git a/build/azure-pipelines/exploration-build.yml b/build/azure-pipelines/exploration-build.yml new file mode 100644 index 0000000000..1fbc9eaa9c --- /dev/null +++ b/build/azure-pipelines/exploration-build.yml @@ -0,0 +1,37 @@ +trigger: + branches: + include: ['master'] +pr: + branches: + include: ['master'] + +steps: +- task: NodeTool@0 + inputs: + versionSpec: "10.15.1" + +- task: AzureKeyVault@1 + displayName: 'Azure Key Vault: Get Secrets' + inputs: + azureSubscription: 'vscode-builds-subscription' + KeyVaultName: vscode + +- script: | + set -e + + cat << EOF > ~/.netrc + machine github.com + login vscode + password $(github-distro-mixin-password) + EOF + + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + + git checkout origin/ben/electron-test + git merge origin/master + + # Push master branch into exploration branch + git push origin HEAD:ben/electron-test + + displayName: Sync & Merge Exploration diff --git a/build/lib/tslint/abstractGlobalsRule.js b/build/lib/tslint/abstractGlobalsRule.js index c870e4d2d3..76e673284b 100644 --- a/build/lib/tslint/abstractGlobalsRule.js +++ b/build/lib/tslint/abstractGlobalsRule.js @@ -19,18 +19,23 @@ class AbstractGlobalsRuleWalker extends Lint.RuleWalker { const checker = this.program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(node); if (symbol) { - const valueDeclaration = symbol.valueDeclaration; - if (valueDeclaration) { - const parent = valueDeclaration.parent; - if (parent) { - const sourceFile = parent.getSourceFile(); - if (sourceFile) { - const fileName = sourceFile.fileName; - if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { - this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); + const declarations = symbol.declarations; + if (Array.isArray(declarations) && symbol.declarations.some(declaration => { + if (declaration) { + const parent = declaration.parent; + if (parent) { + const sourceFile = parent.getSourceFile(); + if (sourceFile) { + const fileName = sourceFile.fileName; + if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { + return true; + } } } } + return false; + })) { + this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); } } } diff --git a/build/lib/tslint/abstractGlobalsRule.ts b/build/lib/tslint/abstractGlobalsRule.ts index 676a34d6db..f1d1eda0f6 100644 --- a/build/lib/tslint/abstractGlobalsRule.ts +++ b/build/lib/tslint/abstractGlobalsRule.ts @@ -30,18 +30,24 @@ export abstract class AbstractGlobalsRuleWalker extends Lint.RuleWalker { const checker = this.program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(node); if (symbol) { - const valueDeclaration = symbol.valueDeclaration; - if (valueDeclaration) { - const parent = valueDeclaration.parent; - if (parent) { - const sourceFile = parent.getSourceFile(); - if (sourceFile) { - const fileName = sourceFile.fileName; - if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { - this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); + const declarations = symbol.declarations; + if (Array.isArray(declarations) && symbol.declarations.some(declaration => { + if (declaration) { + const parent = declaration.parent; + if (parent) { + const sourceFile = parent.getSourceFile(); + if (sourceFile) { + const fileName = sourceFile.fileName; + if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { + return true; + } } } } + + return false; + })) { + this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); } } } diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index e555d61457..3d1049af6b 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -18,7 +18,7 @@ "watch": "gulp watch-extension:configuration-editing" }, "dependencies": { - "jsonc-parser": "2.0.2", + "jsonc-parser": "^2.1.1", "vscode-nls": "^4.0.0" }, "contributes": { diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index ce653f097a..39ad233731 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.8.tgz#fe444203ecef1162348cd6deb76c62477b2cc6e9" integrity sha512-I4+DbJEhLEg4/vIy/2gkWDvXBOOtPKV9EnLhYjMoqxcRW+TTZtUftkHktz/a8suoD5mUL7m6ReLrkPvSsCQQmw== -jsonc-parser@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" - integrity sha512-TSU435K5tEKh3g7bam1AFf+uZrISheoDsLlpmAo6wWZYqjsnd09lHYK1Qo+moK4Ikifev1Gdpa69g4NELKnCrQ== +jsonc-parser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" + integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index 9c07a2568a..50c97fb26f 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -19,7 +19,7 @@ "watch": "gulp watch-extension:extension-editing" }, "dependencies": { - "jsonc-parser": "^2.0.2", + "jsonc-parser": "^2.1.1", "markdown-it": "^8.3.1", "parse5": "^3.0.2", "vscode-nls": "^4.0.0" diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 971bf580ba..fb98728f23 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -29,10 +29,10 @@ entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" integrity sha1-blwtClYhtdra7O+AuQ7ftc13cvA= -jsonc-parser@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" - integrity sha512-TSU435K5tEKh3g7bam1AFf+uZrISheoDsLlpmAo6wWZYqjsnd09lHYK1Qo+moK4Ikifev1Gdpa69g4NELKnCrQ== +jsonc-parser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" + integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== linkify-it@^2.0.0: version "2.0.3" diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index 345263ef17..0655fcdbb2 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -145,11 +145,14 @@ export function activate(context: ExtensionContext) { } }); + const schemaDocuments: { [uri: string]: boolean } = {}; + // handle content request client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { let uri = Uri.parse(uriPath); if (uri.scheme !== 'http' && uri.scheme !== 'https') { return workspace.openTextDocument(uri).then(doc => { + schemaDocuments[uri.toString()] = true; return doc.getText(); }, error => { return Promise.reject(error); @@ -164,10 +167,12 @@ export function activate(context: ExtensionContext) { } }); - let handleContentChange = (uri: Uri) => { - if (uri.scheme === 'vscode' && uri.authority === 'schemas') { - client.sendNotification(SchemaContentChangeNotification.type, uri.toString()); + let handleContentChange = (uriString: string) => { + if (schemaDocuments[uriString]) { + client.sendNotification(SchemaContentChangeNotification.type, uriString); + return true; } + return false; }; let handleActiveEditorChange = (activeEditor?: TextEditor) => { @@ -184,10 +189,13 @@ export function activate(context: ExtensionContext) { } }; - toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri))); + toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); toDispose.push(workspace.onDidCloseTextDocument(d => { - handleContentChange(d.uri); - fileSchemaErrors.delete(d.uri.toString()); + const uriString = d.uri.toString(); + if (handleContentChange(uriString)) { + delete schemaDocuments[uriString]; + } + fileSchemaErrors.delete(uriString); })); toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index bf5e3c8b09..bf76da32db 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -12,7 +12,7 @@ }, "main": "./out/jsonServerMain", "dependencies": { - "jsonc-parser": "^2.1.0", + "jsonc-parser": "^2.1.1", "request-light": "^0.2.4", "vscode-json-languageservice": "^3.3.1", "vscode-languageserver": "^5.3.0-next.8", diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 3bfcb75ac7..3c176e1260 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -59,6 +59,11 @@ jsonc-parser@^2.1.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.0.tgz#eb0d0c7a3c33048524ce3574c57c7278fb2f1bf3" integrity sha512-n9GrT8rrr2fhvBbANa1g+xFmgGK5X91KFeDwlKQ3+SJfmH5+tKv/M/kahx/TXOMflfWHKGKqKyfHQaLKTNzJ6w== +jsonc-parser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" + integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 03a101e660..77627aa57b 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -320,7 +320,7 @@ }, "dependencies": { "highlight.js": "9.15.8", - "markdown-it": "^8.4.2", + "markdown-it": "^9.1.0", "markdown-it-front-matter": "^0.1.2", "vscode-extension-telemetry": "0.1.1", "vscode-nls": "^4.0.0" diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index a1b876e7dc..a45f00885e 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -3900,10 +3900,10 @@ markdown-it-front-matter@^0.1.2: resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.1.2.tgz#e50bf56e77e6a4f5ac4ffa894d4d45ccd9896b20" integrity sha1-5Qv1bnfmpPWsT/qJTU1FzNmJayA= -markdown-it@^8.4.2: - version "8.4.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" - integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ== +markdown-it@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-9.1.0.tgz#df9601c168568704d554b1fff9af0c5b561168d9" + integrity sha512-xHKG4C8iPriyfu/jc2hsCC045fKrMQ0VexX2F1FGYiRxDxqMB2aAhF8WauJ3fltn2kb90moGBkiiEdooGIg55w== dependencies: argparse "^1.0.7" entities "~1.1.1" diff --git a/scripts/code.bat b/scripts/code.bat index 8c058365dc..f4689608e4 100644 --- a/scripts/code.bat +++ b/scripts/code.bat @@ -33,6 +33,7 @@ set VSCODE_CLI=1 set ELECTRON_DEFAULT_ERROR_MODE=1 set ELECTRON_ENABLE_LOGGING=1 set ELECTRON_ENABLE_STACK_DUMPING=1 +set VSCODE_LOGS= :: Launch Code diff --git a/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts b/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts index 7796d52d95..ddaee5fd65 100644 --- a/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts +++ b/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts @@ -22,8 +22,12 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { IFileService } from 'vs/platform/files/common/files'; import * as pfs from 'vs/base/node/pfs'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; class TestEnvironmentService implements IWorkbenchEnvironmentService { + logFile: URI; + options?: IWorkbenchConstructionOptions; + galleryMachineIdResource?: URI; webviewCspSource: string; webviewCspRule: string; localeResource: URI; diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index be42ff6038..48483d8a6c 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1186,22 +1186,14 @@ export function animate(fn: () => void): IDisposable { return toDisposable(() => stepDisposable.dispose()); } - - -const _location = URI.parse(window.location.href); +RemoteAuthorities.setPreferredWebSchema(/^https:/.test(window.location.href) ? 'https' : 'http'); export function asDomUri(uri: URI): URI { if (!uri) { return uri; } if (Schemas.vscodeRemote === uri.scheme) { - if (platform.isWeb) { - // rewrite vscode-remote-uris to uris of the window location - // so that they can be intercepted by the service worker - return _location.with({ path: '/vscode-remote', query: JSON.stringify(uri) }); - } else { - return RemoteAuthorities.rewrite(uri.authority, uri.path); - } + return RemoteAuthorities.rewrite(uri.authority, uri.path); } return uri; } diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index bccc08bd36..628440e6f9 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -197,9 +197,11 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende allowedAttributes: { 'a': ['href', 'name', 'target', 'data-href'], 'iframe': ['allowfullscreen', 'frameborder', 'src'], - 'img': ['src', 'title', 'alt', 'width', 'height'] + 'img': ['src', 'title', 'alt', 'width', 'height'], + 'div': ['class', 'data-code'] } }); + signalInnerHTML!(); return element; diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index 09fc7be707..ae43efd76a 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -24,6 +24,8 @@ export interface IFindInputOptions extends IFindInputStyles { readonly validation?: IInputValidator; readonly label: string; readonly flexibleHeight?: boolean; + readonly flexibleWidth?: boolean; + readonly flexibleMaxHeight?: number; readonly appendCaseSensitiveLabel?: string; readonly appendWholeWordsLabel?: string; @@ -119,6 +121,8 @@ export class FindInput extends Widget { const appendRegexLabel = options.appendRegexLabel || ''; const history = options.history || []; const flexibleHeight = !!options.flexibleHeight; + const flexibleWidth = !!options.flexibleWidth; + const flexibleMaxHeight = options.flexibleMaxHeight; this.domNode = document.createElement('div'); dom.addClass(this.domNode, 'monaco-findInput'); @@ -142,7 +146,9 @@ export class FindInput extends Widget { inputValidationErrorForeground: this.inputValidationErrorForeground, inputValidationErrorBorder: this.inputValidationErrorBorder, history, - flexibleHeight + flexibleHeight, + flexibleWidth, + flexibleMaxHeight })); this.regex = this._register(new RegexCheckbox({ @@ -194,11 +200,7 @@ export class FindInput extends Widget { })); if (this._showOptionButtons) { - const paddingRight = (this.caseSensitive.width() + this.wholeWords.width() + this.regex.width()) + 'px'; - this.inputBox.inputElement.style.paddingRight = paddingRight; - if (this.inputBox.mirrorElement) { - this.inputBox.mirrorElement.style.paddingRight = paddingRight; - } + this.inputBox.paddingRight = this.caseSensitive.width() + this.wholeWords.width() + this.regex.width(); } // Arrow-Key support to navigate between options diff --git a/src/vs/base/browser/ui/findinput/replaceInput.ts b/src/vs/base/browser/ui/findinput/replaceInput.ts new file mode 100644 index 0000000000..141934c785 --- /dev/null +++ b/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -0,0 +1,373 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./findInput'; + +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { IMessage as InputBoxMessage, IInputValidator, IInputBoxStyles, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Color } from 'vs/base/common/color'; +import { ICheckboxStyles, Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { IFindInputCheckboxOpts } from 'vs/base/browser/ui/findinput/findInputCheckboxes'; + +export interface IReplaceInputOptions extends IReplaceInputStyles { + readonly placeholder?: string; + readonly width?: number; + readonly validation?: IInputValidator; + readonly label: string; + readonly flexibleHeight?: boolean; + readonly flexibleWidth?: boolean; + readonly flexibleMaxHeight?: number; + + readonly history?: string[]; +} + +export interface IReplaceInputStyles extends IInputBoxStyles { + inputActiveOptionBorder?: Color; +} + +const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input"); +const NLS_PRESERVE_CASE_LABEL = nls.localize('label.preserveCaseCheckbox', "Preserve Case"); + +export class PreserveCaseCheckbox extends Checkbox { + constructor(opts: IFindInputCheckboxOpts) { + super({ + // TODO: does this need its own icon? + actionClassName: 'monaco-case-sensitive', + title: NLS_PRESERVE_CASE_LABEL + opts.appendTitle, + isChecked: opts.isChecked, + inputActiveOptionBorder: opts.inputActiveOptionBorder + }); + } +} + +export class ReplaceInput extends Widget { + + static readonly OPTION_CHANGE: string = 'optionChange'; + + private contextViewProvider: IContextViewProvider | undefined; + private placeholder: string; + private validation?: IInputValidator; + private label: string; + private fixFocusOnOptionClickEnabled = true; + + private inputActiveOptionBorder?: Color; + private inputBackground?: Color; + private inputForeground?: Color; + private inputBorder?: Color; + + private inputValidationInfoBorder?: Color; + private inputValidationInfoBackground?: Color; + private inputValidationInfoForeground?: Color; + private inputValidationWarningBorder?: Color; + private inputValidationWarningBackground?: Color; + private inputValidationWarningForeground?: Color; + private inputValidationErrorBorder?: Color; + private inputValidationErrorBackground?: Color; + private inputValidationErrorForeground?: Color; + + private preserveCase: PreserveCaseCheckbox; + private cachedOptionsWidth: number = 0; + public domNode: HTMLElement; + public inputBox: HistoryInputBox; + + private readonly _onDidOptionChange = this._register(new Emitter()); + public readonly onDidOptionChange: Event = this._onDidOptionChange.event; + + private readonly _onKeyDown = this._register(new Emitter()); + public readonly onKeyDown: Event = this._onKeyDown.event; + + private readonly _onMouseDown = this._register(new Emitter()); + public readonly onMouseDown: Event = this._onMouseDown.event; + + private readonly _onInput = this._register(new Emitter()); + public readonly onInput: Event = this._onInput.event; + + private readonly _onKeyUp = this._register(new Emitter()); + public readonly onKeyUp: Event = this._onKeyUp.event; + + private _onPreserveCaseKeyDown = this._register(new Emitter()); + public readonly onPreserveCaseKeyDown: Event = this._onPreserveCaseKeyDown.event; + + constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, private readonly _showOptionButtons: boolean, options: IReplaceInputOptions) { + super(); + this.contextViewProvider = contextViewProvider; + this.placeholder = options.placeholder || ''; + this.validation = options.validation; + this.label = options.label || NLS_DEFAULT_LABEL; + + this.inputActiveOptionBorder = options.inputActiveOptionBorder; + this.inputBackground = options.inputBackground; + this.inputForeground = options.inputForeground; + this.inputBorder = options.inputBorder; + + this.inputValidationInfoBorder = options.inputValidationInfoBorder; + this.inputValidationInfoBackground = options.inputValidationInfoBackground; + this.inputValidationInfoForeground = options.inputValidationInfoForeground; + this.inputValidationWarningBorder = options.inputValidationWarningBorder; + this.inputValidationWarningBackground = options.inputValidationWarningBackground; + this.inputValidationWarningForeground = options.inputValidationWarningForeground; + this.inputValidationErrorBorder = options.inputValidationErrorBorder; + this.inputValidationErrorBackground = options.inputValidationErrorBackground; + this.inputValidationErrorForeground = options.inputValidationErrorForeground; + + const flexibleHeight = !!options.flexibleHeight; + const flexibleWidth = !!options.flexibleWidth; + const flexibleMaxHeight = options.flexibleMaxHeight; + + this.buildDomNode(options.history || [], flexibleHeight, flexibleWidth, flexibleMaxHeight); + + if (parent) { + parent.appendChild(this.domNode); + } + + this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e)); + this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e)); + this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire()); + this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e)); + } + + public enable(): void { + dom.removeClass(this.domNode, 'disabled'); + this.inputBox.enable(); + this.preserveCase.enable(); + } + + public disable(): void { + dom.addClass(this.domNode, 'disabled'); + this.inputBox.disable(); + this.preserveCase.disable(); + } + + public setFocusInputOnOptionClick(value: boolean): void { + this.fixFocusOnOptionClickEnabled = value; + } + + public setEnabled(enabled: boolean): void { + if (enabled) { + this.enable(); + } else { + this.disable(); + } + } + + public clear(): void { + this.clearValidation(); + this.setValue(''); + this.focus(); + } + + public getValue(): string { + return this.inputBox.value; + } + + public setValue(value: string): void { + if (this.inputBox.value !== value) { + this.inputBox.value = value; + } + } + + public onSearchSubmit(): void { + this.inputBox.addToHistory(); + } + + public style(styles: IReplaceInputStyles): void { + this.inputActiveOptionBorder = styles.inputActiveOptionBorder; + this.inputBackground = styles.inputBackground; + this.inputForeground = styles.inputForeground; + this.inputBorder = styles.inputBorder; + + this.inputValidationInfoBackground = styles.inputValidationInfoBackground; + this.inputValidationInfoForeground = styles.inputValidationInfoForeground; + this.inputValidationInfoBorder = styles.inputValidationInfoBorder; + this.inputValidationWarningBackground = styles.inputValidationWarningBackground; + this.inputValidationWarningForeground = styles.inputValidationWarningForeground; + this.inputValidationWarningBorder = styles.inputValidationWarningBorder; + this.inputValidationErrorBackground = styles.inputValidationErrorBackground; + this.inputValidationErrorForeground = styles.inputValidationErrorForeground; + this.inputValidationErrorBorder = styles.inputValidationErrorBorder; + + this.applyStyles(); + } + + protected applyStyles(): void { + if (this.domNode) { + const checkBoxStyles: ICheckboxStyles = { + inputActiveOptionBorder: this.inputActiveOptionBorder, + }; + this.preserveCase.style(checkBoxStyles); + + const inputBoxStyles: IInputBoxStyles = { + inputBackground: this.inputBackground, + inputForeground: this.inputForeground, + inputBorder: this.inputBorder, + inputValidationInfoBackground: this.inputValidationInfoBackground, + inputValidationInfoForeground: this.inputValidationInfoForeground, + inputValidationInfoBorder: this.inputValidationInfoBorder, + inputValidationWarningBackground: this.inputValidationWarningBackground, + inputValidationWarningForeground: this.inputValidationWarningForeground, + inputValidationWarningBorder: this.inputValidationWarningBorder, + inputValidationErrorBackground: this.inputValidationErrorBackground, + inputValidationErrorForeground: this.inputValidationErrorForeground, + inputValidationErrorBorder: this.inputValidationErrorBorder + }; + this.inputBox.style(inputBoxStyles); + } + } + + public select(): void { + this.inputBox.select(); + } + + public focus(): void { + this.inputBox.focus(); + } + + public getPreserveCase(): boolean { + return this.preserveCase.checked; + } + + public setPreserveCase(value: boolean): void { + this.preserveCase.checked = value; + } + + public focusOnPreserve(): void { + this.preserveCase.focus(); + } + + private _lastHighlightFindOptions: number = 0; + public highlightFindOptions(): void { + dom.removeClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions)); + this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions; + dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions)); + } + + private buildDomNode(history: string[], flexibleHeight: boolean, flexibleWidth: boolean, flexibleMaxHeight: number | undefined): void { + this.domNode = document.createElement('div'); + dom.addClass(this.domNode, 'monaco-findInput'); + + this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, { + ariaLabel: this.label || '', + placeholder: this.placeholder || '', + validationOptions: { + validation: this.validation + }, + inputBackground: this.inputBackground, + inputForeground: this.inputForeground, + inputBorder: this.inputBorder, + inputValidationInfoBackground: this.inputValidationInfoBackground, + inputValidationInfoForeground: this.inputValidationInfoForeground, + inputValidationInfoBorder: this.inputValidationInfoBorder, + inputValidationWarningBackground: this.inputValidationWarningBackground, + inputValidationWarningForeground: this.inputValidationWarningForeground, + inputValidationWarningBorder: this.inputValidationWarningBorder, + inputValidationErrorBackground: this.inputValidationErrorBackground, + inputValidationErrorForeground: this.inputValidationErrorForeground, + inputValidationErrorBorder: this.inputValidationErrorBorder, + history, + flexibleHeight, + flexibleWidth, + flexibleMaxHeight + })); + + this.preserveCase = this._register(new PreserveCaseCheckbox({ + appendTitle: '', + isChecked: false, + inputActiveOptionBorder: this.inputActiveOptionBorder + })); + this._register(this.preserveCase.onChange(viaKeyboard => { + this._onDidOptionChange.fire(viaKeyboard); + if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) { + this.inputBox.focus(); + } + this.validate(); + })); + this._register(this.preserveCase.onKeyDown(e => { + this._onPreserveCaseKeyDown.fire(e); + })); + + if (this._showOptionButtons) { + this.cachedOptionsWidth = this.preserveCase.width(); + } else { + this.cachedOptionsWidth = 0; + } + + // Arrow-Key support to navigate between options + let indexes = [this.preserveCase.domNode]; + this.onkeydown(this.domNode, (event: IKeyboardEvent) => { + if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Escape)) { + let index = indexes.indexOf(document.activeElement); + if (index >= 0) { + let newIndex: number = -1; + if (event.equals(KeyCode.RightArrow)) { + newIndex = (index + 1) % indexes.length; + } else if (event.equals(KeyCode.LeftArrow)) { + if (index === 0) { + newIndex = indexes.length - 1; + } else { + newIndex = index - 1; + } + } + + if (event.equals(KeyCode.Escape)) { + indexes[index].blur(); + } else if (newIndex >= 0) { + indexes[newIndex].focus(); + } + + dom.EventHelper.stop(event, true); + } + } + }); + + + let controls = document.createElement('div'); + controls.className = 'controls'; + controls.style.display = this._showOptionButtons ? 'block' : 'none'; + controls.appendChild(this.preserveCase.domNode); + + this.domNode.appendChild(controls); + } + + public validate(): void { + if (this.inputBox) { + this.inputBox.validate(); + } + } + + public showMessage(message: InputBoxMessage): void { + if (this.inputBox) { + this.inputBox.showMessage(message); + } + } + + public clearMessage(): void { + if (this.inputBox) { + this.inputBox.hideMessage(); + } + } + + private clearValidation(): void { + if (this.inputBox) { + this.inputBox.hideMessage(); + } + } + + public set width(newWidth: number) { + this.inputBox.paddingRight = this.cachedOptionsWidth; + this.inputBox.width = newWidth; + this.domNode.style.width = newWidth + 'px'; + } + + public dispose(): void { + super.dispose(); + } +} diff --git a/src/vs/base/browser/ui/inputbox/inputBox.css b/src/vs/base/browser/ui/inputbox/inputBox.css index d429251c8f..21ab84944d 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.css +++ b/src/vs/base/browser/ui/inputbox/inputBox.css @@ -60,6 +60,8 @@ display: block; -ms-overflow-style: none; /* IE 10+ */ overflow: -moz-scrollbars-none; /* Firefox */ + scrollbar-width: none; /* Firefox ^64 */ + outline: none; } .monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar { diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index b78389a77b..d03467b8d3 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -32,6 +32,7 @@ export interface IInputOptions extends IInputBoxStyles { readonly type?: string; readonly validationOptions?: IInputValidationOptions; readonly flexibleHeight?: boolean; + readonly flexibleWidth?: boolean; readonly flexibleMaxHeight?: number; readonly actions?: ReadonlyArray; @@ -185,6 +186,13 @@ export class InputBox extends Widget { this.mirror.innerHTML = ' '; this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto }); + + if (this.options.flexibleWidth) { + this.input.setAttribute('wrap', 'off'); + this.mirror.style.whiteSpace = 'pre'; + this.mirror.style.wordWrap = 'initial'; + } + dom.append(container, this.scrollableElement.getDomNode()); this._register(this.scrollableElement); @@ -345,12 +353,36 @@ export class InputBox extends Widget { } public set width(width: number) { - this.input.style.width = width + 'px'; + if (this.options.flexibleHeight && this.options.flexibleWidth) { + // textarea with horizontal scrolling + let horizontalPadding = 0; + if (this.mirror) { + const paddingLeft = parseFloat(this.mirror.style.paddingLeft || '') || 0; + const paddingRight = parseFloat(this.mirror.style.paddingRight || '') || 0; + horizontalPadding = paddingLeft + paddingRight; + } + this.input.style.width = (width - horizontalPadding) + 'px'; + } else { + this.input.style.width = width + 'px'; + } + if (this.mirror) { this.mirror.style.width = width + 'px'; } } + public set paddingRight(paddingRight: number) { + if (this.options.flexibleHeight && this.options.flexibleWidth) { + this.input.style.width = `calc(100% - ${paddingRight}px)`; + } else { + this.input.style.paddingRight = paddingRight + 'px'; + } + + if (this.mirror) { + this.mirror.style.paddingRight = paddingRight + 'px'; + } + } + private updateScrollDimensions(): void { if (typeof this.cachedContentHeight !== 'number' || typeof this.cachedHeight !== 'number') { return; diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index a73f94d79d..5054251c3d 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -683,6 +683,22 @@ export class ListView implements ISpliceable, IDisposable { this.scrollableElement.setScrollPosition({ scrollTop }); } + getScrollLeft(): number { + const scrollPosition = this.scrollableElement.getScrollPosition(); + return scrollPosition.scrollLeft; + } + + setScrollLeftt(scrollLeft: number): void { + if (this.scrollableElementUpdateDisposable) { + this.scrollableElementUpdateDisposable.dispose(); + this.scrollableElementUpdateDisposable = null; + this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth }); + } + + this.scrollableElement.setScrollPosition({ scrollLeft }); + } + + get scrollTop(): number { return this.getScrollTop(); } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 6fadebb8f8..19b844bc13 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1307,6 +1307,14 @@ export class List implements ISpliceable, IDisposable { this.view.setScrollTop(scrollTop); } + get scrollLeft(): number { + return this.view.getScrollLeft(); + } + + set scrollLeft(scrollLeft: number) { + this.view.setScrollLeftt(scrollLeft); + } + get scrollHeight(): number { return this.view.scrollHeight; } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 87328f2f2b..5204ced912 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1319,6 +1319,14 @@ export abstract class AbstractTree implements IDisposable this.view.scrollTop = scrollTop; } + get scrollLeft(): number { + return this.view.scrollTop; + } + + set scrollLeft(scrollLeft: number) { + this.view.scrollLeft = scrollLeft; + } + get scrollHeight(): number { return this.view.scrollHeight; } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 958bb3d60a..14580cd07c 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -400,6 +400,14 @@ export class AsyncDataTree implements IDisposable this.tree.scrollTop = scrollTop; } + get scrollLeft(): number { + return this.tree.scrollLeft; + } + + set scrollLeft(scrollLeft: number) { + this.tree.scrollLeft = scrollLeft; + } + get scrollHeight(): number { return this.tree.scrollHeight; } diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index c6b907087e..19954afdf5 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -219,7 +219,7 @@ export class IndexTreeModel, TFilterData = voi const result = this._setListNodeCollapsed(node, listIndex, revealed, collapsed!, recursive || false); - if (this.autoExpandSingleChildren && !collapsed! && !recursive) { + if (node !== this.root && this.autoExpandSingleChildren && !collapsed! && !recursive) { let onlyVisibleChildIndex = -1; for (let i = 0; i < node.children.length; i++) { diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index c728a13721..0159591a39 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -549,7 +549,7 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb const patternStartPos = patternPos; const wordStartPos = wordPos; - // There will be a mach, fill in tables + // There will be a match, fill in tables for (patternPos = patternStartPos + 1; patternPos <= patternLen; patternPos++) { for (wordPos = 1; wordPos <= wordLen; wordPos++) { @@ -573,6 +573,11 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb } else { score = 5; } + } else if (isSeparatorAtPos(wordLow, wordPos - 1) && (wordPos === 1 || !isSeparatorAtPos(wordLow, wordPos - 2))) { + // hitting a separator: `. <-> foo.bar` + // ^ + score = 5; + } else if (isSeparatorAtPos(wordLow, wordPos - 2) || isWhitespaceAtPos(wordLow, wordPos - 2)) { // post separator: `foo <-> bar_foo` // ^^^ diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 258dd79a64..29255182d1 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; +import * as platform from 'vs/base/common/platform'; export namespace Schemas { @@ -58,11 +59,17 @@ class RemoteAuthoritiesImpl { private readonly _hosts: { [authority: string]: string; }; private readonly _ports: { [authority: string]: number; }; private readonly _connectionTokens: { [authority: string]: string; }; + private _preferredWebSchema: 'http' | 'https'; constructor() { this._hosts = Object.create(null); this._ports = Object.create(null); this._connectionTokens = Object.create(null); + this._preferredWebSchema = 'http'; + } + + public setPreferredWebSchema(schema: 'http' | 'https') { + this._preferredWebSchema = schema; } public set(authority: string, host: string, port: number): void { @@ -79,9 +86,9 @@ class RemoteAuthoritiesImpl { const port = this._ports[authority]; const connectionToken = this._connectionTokens[authority]; return URI.from({ - scheme: Schemas.vscodeRemoteResource, + scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource, authority: `${host}:${port}`, - path: `/vscode-remote2`, + path: `/vscode-remote-resource`, query: `path=${encodeURIComponent(path)}&tkn=${encodeURIComponent(connectionToken)}` }); } diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 129ba8c39e..ebb8bf854b 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -366,6 +366,10 @@ suite('Filters', () => { assertMatches('f', ':foo', ':^foo', fuzzyScore); }); + test('Separator only match should not be weak #79558', function () { + assertMatches('.', 'foo.bar', 'foo^.bar', fuzzyScore); + }); + test('Cannot set property \'1\' of undefined, #26511', function () { let word = new Array(123).join('a'); let pattern = new Array(120).join('a'); diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 8f8f45fb15..9bf87281e2 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -25,8 +25,8 @@ - + - + diff --git a/src/vs/code/browser/workbench/workbench.js b/src/vs/code/browser/workbench/workbench.js index 9dd81ca5e7..815fb0ea05 100644 --- a/src/vs/code/browser/workbench/workbench.js +++ b/src/vs/code/browser/workbench/workbench.js @@ -8,15 +8,15 @@ (function () { require.config({ - baseUrl: `${window.location.origin}/out`, + baseUrl: `${window.location.origin}/static/out`, paths: { - 'vscode-textmate': `${window.location.origin}/node_modules/vscode-textmate/release/main`, - 'onigasm-umd': `${window.location.origin}/node_modules/onigasm-umd/release/main`, - 'xterm': `${window.location.origin}/node_modules/xterm/lib/xterm.js`, - 'xterm-addon-search': `${window.location.origin}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, - 'xterm-addon-web-links': `${window.location.origin}/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, - 'semver-umd': `${window.location.origin}/node_modules/semver-umd/lib/semver-umd.js`, - '@microsoft/applicationinsights-web': `${window.location.origin}/node_modules/@microsoft/applicationinsights-web/dist/applicationinsights-web.js`, + 'vscode-textmate': `${window.location.origin}/static/node_modules/vscode-textmate/release/main`, + 'onigasm-umd': `${window.location.origin}/static/node_modules/onigasm-umd/release/main`, + 'xterm': `${window.location.origin}/static/node_modules/xterm/lib/xterm.js`, + 'xterm-addon-search': `${window.location.origin}/static/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, + 'xterm-addon-web-links': `${window.location.origin}/static/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, + 'semver-umd': `${window.location.origin}/static/node_modules/semver-umd/lib/semver-umd.js`, + '@microsoft/applicationinsights-web': `${window.location.origin}/static/node_modules/@microsoft/applicationinsights-web/dist/applicationinsights-web.js`, } }); diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 72a78cd049..51d883f386 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -4,41 +4,39 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; import * as resources from 'vs/base/common/resources'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IOpenerService, IOpener } from 'vs/platform/opener/common/opener'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { LinkedList } from 'vs/base/common/linkedList'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { localize } from 'vs/nls'; -import { IProductService } from 'vs/platform/product/common/product'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import Severity from 'vs/base/common/severity'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { IOpener, IOpenerService, IValidator } from 'vs/platform/opener/common/opener'; -export class OpenerService implements IOpenerService { +export class OpenerService extends Disposable implements IOpenerService { _serviceBrand!: ServiceIdentifier; - private readonly _opener = new LinkedList(); + private readonly _openers = new LinkedList(); + private readonly _validators = new LinkedList(); constructor( @ICodeEditorService private readonly _editorService: ICodeEditorService, @ICommandService private readonly _commandService: ICommandService, - @IStorageService private readonly _storageService: IStorageService, - @IDialogService private readonly _dialogService: IDialogService, - @IProductService private readonly _productService: IProductService ) { - // + super(); } registerOpener(opener: IOpener): IDisposable { - const remove = this._opener.push(opener); + const remove = this._openers.push(opener); + return { dispose: remove }; + } + + registerValidator(validator: IValidator): IDisposable { + const remove = this._validators.push(validator); return { dispose: remove }; } @@ -47,8 +45,16 @@ export class OpenerService implements IOpenerService { if (!resource.scheme) { return Promise.resolve(false); } + + // check with contributed validators + for (const validator of this._validators.toArray()) { + if (!(await validator.shouldOpen(resource))) { + return false; + } + } + // check with contributed openers - for (const opener of this._opener.toArray()) { + for (const opener of this._openers.toArray()) { const handled = await opener.open(resource, options); if (handled) { return true; @@ -60,7 +66,7 @@ export class OpenerService implements IOpenerService { private _doOpen(resource: URI, options?: { openToSide?: boolean, openExternal?: boolean }): Promise { - const { scheme, authority, path, query, fragment } = resource; + const { scheme, path, query, fragment } = resource; if (equalsIgnoreCase(scheme, Schemas.mailto) || (options && options.openExternal)) { // open default mail application @@ -68,48 +74,8 @@ export class OpenerService implements IOpenerService { } if (equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https)) { - let trustedDomains: string[] = ['https://code.visualstudio.com']; - try { - const trustedDomainsSrc = this._storageService.get('http.trustedDomains', StorageScope.GLOBAL); - if (trustedDomainsSrc) { - trustedDomains = JSON.parse(trustedDomainsSrc); - } - } catch (err) { } - - const domainToOpen = `${scheme}://${authority}`; - - if (isDomainTrusted(domainToOpen, trustedDomains)) { - return this._doOpenExternal(resource); - } else { - return this._dialogService.show( - Severity.Info, - localize( - 'openExternalLinkAt', - 'Do you want {0} to open the external website?\n{1}', - this._productService.nameShort, - resource.toString(true) - ), - [ - localize('openLink', 'Open Link'), - localize('cancel', 'Cancel'), - localize('configureTrustedDomains', 'Configure Trusted Domains') - ], - { - cancelId: 1 - }).then((choice) => { - if (choice === 0) { - return this._doOpenExternal(resource); - } else if (choice === 2) { - return this._commandService.executeCommand('workbench.action.configureTrustedDomains', domainToOpen).then((pickedDomains: string[]) => { - if (pickedDomains.indexOf(domainToOpen) !== -1) { - return this._doOpenExternal(resource); - } - return Promise.resolve(false); - }); - } - return Promise.resolve(false); - }); - } + // open link in default browser + return this._doOpenExternal(resource); } else if (equalsIgnoreCase(scheme, Schemas.command)) { // run command or bail out if command isn't known if (!CommandsRegistry.getCommand(path)) { @@ -158,22 +124,8 @@ export class OpenerService implements IOpenerService { return Promise.resolve(true); } -} -/** - * Check whether a domain like https://www.microsoft.com matches - * the list of trusted domains. - */ -function isDomainTrusted(domain: string, trustedDomains: string[]) { - for (let i = 0; i < trustedDomains.length; i++) { - if (trustedDomains[i] === '*') { - return true; - } - - if (trustedDomains[i] === domain) { - return true; - } + dispose() { + this._validators.clear(); } - - return false; } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 1d504f28f7..9b60e7e626 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -367,6 +367,10 @@ export let completionKindFromString: { }; })(); +export const enum CompletionItemKindModifier { + Deprecated = 1 +} + export const enum CompletionItemInsertTextRule { /** * Adjust whitespace/indentation of multiline insert texts to @@ -396,6 +400,11 @@ export interface CompletionItem { * an icon is chosen by the editor. */ kind: CompletionItemKind; + /** + * A modifier to the `kind` which affect how the item + * is rendered, e.g. Deprecated is rendered with a strikeout + */ + kindModifier?: CompletionItemKindModifier; /** * A human-readable string with additional information * about this item, like type or symbol information. @@ -464,7 +473,7 @@ export interface CompletionItem { /** * @internal */ - [key: string]: any; + _id?: [number, number]; } export interface CompletionList { diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index d2864eb9a1..9b06b322e4 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -581,6 +581,10 @@ export enum CompletionItemKind { Snippet = 25 } +export enum CompletionItemKindModifier { + Deprecated = 1 +} + export enum CompletionItemInsertTextRule { /** * Adjust whitespace/indentation of multiline insert texts to diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index f34e82f441..5cb0132ad3 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -12,19 +12,20 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; +import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding, CONTEXT_REPLACE_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel'; import { FindOptionsWidget } from 'vs/editor/contrib/find/findOptionsWidget'; import { FindReplaceState, FindReplaceStateChangedEvent, INewFindReplaceState } from 'vs/editor/contrib/find/findState'; import { FindWidget, IFindController } from 'vs/editor/contrib/find/findWidget'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; const SEARCH_STRING_MAX_LENGTH = 524288; @@ -75,7 +76,7 @@ export class CommonFindController extends Disposable implements editorCommon.IEd protected _state: FindReplaceState; protected _updateHistoryDelayer: Delayer; private _model: FindModelBoundToEditorModel | null; - private readonly _storageService: IStorageService; + protected readonly _storageService: IStorageService; private readonly _clipboardService: IClipboardService; protected readonly _contextKeyService: IContextKeyService; @@ -383,10 +384,11 @@ export class FindController extends CommonFindController implements IFindControl @IContextKeyService _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IThemeService private readonly _themeService: IThemeService, - @IStorageService storageService: IStorageService, - @optional(IClipboardService) clipboardService: IClipboardService + @INotificationService private readonly _notificationService: INotificationService, + @IStorageService _storageService: IStorageService, + @optional(IClipboardService) clipboardService: IClipboardService, ) { - super(editor, _contextKeyService, storageService, clipboardService); + super(editor, _contextKeyService, _storageService, clipboardService); this._widget = null; this._findOptionsWidget = null; } @@ -422,7 +424,7 @@ export class FindController extends CommonFindController implements IFindControl } private _createFindWidget() { - this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService)); + this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService)); this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService, this._themeService)); } } @@ -540,6 +542,27 @@ export class NextMatchFindAction extends MatchFindAction { } } +export class NextMatchFindAction2 extends MatchFindAction { + + constructor() { + super({ + id: FIND_IDS.NextMatchFindAction, + label: nls.localize('findNextMatchAction', "Find Next"), + alias: 'Find Next', + precondition: undefined, + kbOpts: { + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } + }); + } + + protected _run(controller: CommonFindController): boolean { + return controller.moveToNextMatch(); + } +} + export class PreviousMatchFindAction extends MatchFindAction { constructor() { @@ -562,6 +585,27 @@ export class PreviousMatchFindAction extends MatchFindAction { } } +export class PreviousMatchFindAction2 extends MatchFindAction { + + constructor() { + super({ + id: FIND_IDS.PreviousMatchFindAction, + label: nls.localize('findPreviousMatchAction', "Find Previous"), + alias: 'Find Previous', + precondition: undefined, + kbOpts: { + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED), + primary: KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } + }); + } + + protected _run(controller: CommonFindController): boolean { + return controller.moveToPrevMatch(); + } +} + export abstract class SelectionMatchFindAction extends EditorAction { public run(accessor: ServicesAccessor | null, editor: ICodeEditor): void { let controller = CommonFindController.get(editor); @@ -695,7 +739,9 @@ registerEditorContribution(FindController); registerEditorAction(StartFindAction); registerEditorAction(StartFindWithSelectionAction); registerEditorAction(NextMatchFindAction); +registerEditorAction(NextMatchFindAction2); registerEditorAction(PreviousMatchFindAction); +registerEditorAction(PreviousMatchFindAction2); registerEditorAction(NextSelectionMatchFindAction); registerEditorAction(PreviousSelectionMatchFindAction); registerEditorAction(StartFindReplaceAction); @@ -781,6 +827,17 @@ registerEditorCommand(new FindCommand({ } })); +registerEditorCommand(new FindCommand({ + id: FIND_IDS.ReplaceOneAction, + precondition: CONTEXT_FIND_WIDGET_VISIBLE, + handler: x => x.replace(), + kbOpts: { + weight: KeybindingWeight.EditorContrib + 5, + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED), + primary: KeyCode.Enter + } +})); + registerEditorCommand(new FindCommand({ id: FIND_IDS.ReplaceAllAction, precondition: CONTEXT_FIND_WIDGET_VISIBLE, @@ -792,6 +849,20 @@ registerEditorCommand(new FindCommand({ } })); +registerEditorCommand(new FindCommand({ + id: FIND_IDS.ReplaceAllAction, + precondition: CONTEXT_FIND_WIDGET_VISIBLE, + handler: x => x.replaceAll(), + kbOpts: { + weight: KeybindingWeight.EditorContrib + 5, + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED), + primary: undefined, + mac: { + primary: KeyMod.CtrlCmd | KeyCode.Enter, + } + } +})); + registerEditorCommand(new FindCommand({ id: FIND_IDS.SelectAllMatchesAction, precondition: CONTEXT_FIND_WIDGET_VISIBLE, diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index 4cd424b441..a2a3987e79 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -32,8 +32,8 @@ .monaco-editor .find-widget { position: absolute; z-index: 10; - top: -44px; /* find input height + shadow (10px) */ - height: 34px; /* find input height */ + top: -44px; + height: 33px; overflow: hidden; line-height: 19px; transition: top 200ms linear; @@ -47,12 +47,10 @@ /* Find widget when replace is toggled on */ .monaco-editor .find-widget.replaceToggled { top: -74px; /* find input height + replace input height + shadow (10px) */ - height: 64px; /* find input height + replace input height */ } .monaco-editor .find-widget.replaceToggled > .replace-part { display: flex; display: -webkit-flex; - align-items: center; } .monaco-editor .find-widget.visible, @@ -60,13 +58,31 @@ top: 0; } +/* Multiple line find widget */ + +.monaco-editor .find-widget.multipleline { + top: unset; + bottom: 10px; +} + +.monaco-editor .find-widget.multipleline.visible, +.monaco-editor .find-widget.multipleline.replaceToggled.visible { + top: 0px; + bottom: unset; +} + +.monaco-editor .find-widget .monaco-inputbox.synthetic-focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; +} + .monaco-editor .find-widget .monaco-inputbox .input { background-color: transparent; /* Style to compensate for //winjs */ min-height: 0; } -.monaco-editor .find-widget .replace-input .input { +.monaco-editor .find-widget .monaco-findInput .input { font-size: 13px; } @@ -76,29 +92,38 @@ font-size: 12px; display: flex; display: -webkit-flex; - align-items: center; } .monaco-editor .find-widget > .find-part .monaco-inputbox, .monaco-editor .find-widget > .replace-part .monaco-inputbox { - height: 25px; + min-height: 25px; } -.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input { - width: 100% !important; - padding-right: 66px; -} -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input { +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { padding-right: 22px; } .monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input, -.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input { +.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .mirror, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input, +.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror { padding-top: 2px; padding-bottom: 2px; } +.monaco-editor .find-widget > .find-part .find-actions { + height: 25px; + display: flex; + align-items: center; +} + +.monaco-editor .find-widget > .replace-part .replace-actions { + height: 25px; + display: flex; + align-items: center; +} + .monaco-editor .find-widget .monaco-findInput { vertical-align: middle; display: flex; @@ -106,6 +131,16 @@ flex:1; } +.monaco-editor .find-widget .monaco-findInput .monaco-scrollable-element { + /* Make sure textarea inherits the width correctly */ + width: 100%; +} + +.monaco-editor .find-widget .monaco-findInput .monaco-scrollable-element .scrollbar.vertical { + /* Hide vertical scrollbar */ + opacity: 0; +} + .monaco-editor .find-widget .matchesCount { display: flex; display: -webkit-flex; @@ -237,15 +272,17 @@ display: none; } -.monaco-editor .find-widget > .replace-part > .replace-input { +.monaco-editor .find-widget > .replace-part > .monaco-findInput { position: relative; display: flex; display: -webkit-flex; vertical-align: middle; - width: auto !important; + flex: auto; + flex-grow: 0; + flex-shrink: 0; } -.monaco-editor .find-widget > .replace-part > .replace-input > .controls { +.monaco-editor .find-widget > .replace-part > .monaco-findInput > .controls { position: absolute; top: 3px; right: 2px; diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 24bc0f88ea..b4f70da0f7 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -9,11 +9,12 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; -import { HistoryInputBox, IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; +import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; import { IHorizontalSashLayoutProvider, ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { Widget } from 'vs/base/browser/ui/widget'; -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { Delayer } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -28,11 +29,12 @@ import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_REPLACE_INPUT_FOCUSED, FIND_IDS, MA import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { contrastBorder, editorFindMatch, editorFindMatchBorder, editorFindMatchHighlight, editorFindMatchHighlightBorder, editorFindRangeHighlight, editorFindRangeHighlightBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetResizeBorder, errorForeground, inputActiveOptionBorder, inputActiveOptionBackground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorFindMatch, editorFindMatchBorder, editorFindMatchHighlight, editorFindMatchHighlightBorder, editorFindRangeHighlight, editorFindRangeHighlightBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetResizeBorder, errorForeground, inputActiveOptionBorder, inputActiveOptionBackground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { ContextScopedFindInput, ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget'; +import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; -import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export interface IFindController { replace(): void; @@ -48,7 +50,6 @@ const NLS_TOGGLE_SELECTION_FIND_TITLE = nls.localize('label.toggleSelectionFind' const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace"); const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Replace"); -const NLS_PRESERVE_CASE_LABEL = nls.localize('label.preserveCaseCheckbox', "Preserve Case"); const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace"); const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode"); @@ -59,14 +60,12 @@ const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results"); const FIND_WIDGET_INITIAL_WIDTH = 411; const PART_WIDTH = 275; const FIND_INPUT_AREA_WIDTH = PART_WIDTH - 54; -const REPLACE_INPUT_AREA_WIDTH = FIND_INPUT_AREA_WIDTH; let MAX_MATCHES_COUNT_WIDTH = 69; let FIND_ALL_CONTROLS_WIDTH = 17/** Find Input margin-left */ + (MAX_MATCHES_COUNT_WIDTH + 3 + 1) /** Match Results */ + 23 /** Button */ * 4 + 2/** sash */; -const FIND_INPUT_AREA_HEIGHT = 34; // The height of Find Widget when Replace Input is not visible. -const FIND_REPLACE_AREA_HEIGHT = 64; // The height of Find Widget when Replace Input is visible. - +const FIND_INPUT_AREA_HEIGHT = 33; // The height of Find Widget when Replace Input is not visible. +const ctrlEnterReplaceAllWarningPromptedKey = 'ctrlEnterReplaceAll.windows.donotask'; export class FindWidgetViewZone implements IViewZone { public readonly afterLineNumber: number; @@ -84,6 +83,22 @@ export class FindWidgetViewZone implements IViewZone { } } +function stopPropagationForMultiLineUpwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) { + const isMultiline = !!value.match(/\n/); + if (textarea && isMultiline && textarea.selectionStart > 0) { + event.stopPropagation(); + return; + } +} + +function stopPropagationForMultiLineDownwards(event: IKeyboardEvent, value: string, textarea: HTMLTextAreaElement | null) { + const isMultiline = !!value.match(/\n/); + if (textarea && isMultiline && textarea.selectionEnd < textarea.value.length) { + event.stopPropagation(); + return; + } +} + export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSashLayoutProvider { private static readonly ID = 'editor.contrib.findWidget'; private readonly _codeEditor: ICodeEditor; @@ -92,10 +107,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private readonly _contextViewProvider: IContextViewProvider; private readonly _keybindingService: IKeybindingService; private readonly _contextKeyService: IContextKeyService; + private readonly _storageService: IStorageService; + private readonly _notificationService: INotificationService; private _domNode!: HTMLElement; + private _cachedHeight: number | null; private _findInput!: FindInput; - private _replaceInputBox!: HistoryInputBox; + private _replaceInput!: ReplaceInput; private _toggleReplaceBtn!: SimpleButton; private _matchesCount!: HTMLElement; @@ -103,13 +121,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _nextBtn!: SimpleButton; private _toggleSelectionFind!: SimpleCheckbox; private _closeBtn!: SimpleButton; - private _preserveCase!: Checkbox; private _replaceBtn!: SimpleButton; private _replaceAllBtn!: SimpleButton; private _isVisible: boolean; private _isReplaceVisible: boolean; private _ignoreChangeEvent: boolean; + private _ctrlEnterReplaceAllWarningPrompted: boolean; private readonly _findFocusTracker: dom.IFocusTracker; private readonly _findInputFocused: IContextKey; @@ -129,7 +147,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas contextViewProvider: IContextViewProvider, keybindingService: IKeybindingService, contextKeyService: IContextKeyService, - themeService: IThemeService + themeService: IThemeService, + storageService: IStorageService, + notificationService: INotificationService, ) { super(); this._codeEditor = codeEditor; @@ -138,6 +158,10 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._contextViewProvider = contextViewProvider; this._keybindingService = keybindingService; this._contextKeyService = contextKeyService; + this._storageService = storageService; + this._notificationService = notificationService; + + this._ctrlEnterReplaceAllWarningPrompted = !!storageService.getBoolean(ctrlEnterReplaceAllWarningPromptedKey, StorageScope.GLOBAL); this._isVisible = false; this._isReplaceVisible = false; @@ -149,6 +173,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._buildDomNode(); this._updateButtons(); this._tryUpdateWidgetWidth(); + this._findInput.inputBox.layout(); this._register(this._codeEditor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => { if (e.readOnly) { @@ -203,7 +228,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas })); this._replaceInputFocused = CONTEXT_REPLACE_INPUT_FOCUSED.bindTo(contextKeyService); - this._replaceFocusTracker = this._register(dom.trackFocus(this._replaceInputBox.inputElement)); + this._replaceFocusTracker = this._register(dom.trackFocus(this._replaceInput.inputBox.inputElement)); this._register(this._replaceFocusTracker.onDidFocus(() => { this._replaceInputFocused.set(true); this._updateSearchScope(); @@ -264,6 +289,12 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _onStateChanged(e: FindReplaceStateChangedEvent): void { if (e.searchString) { + if (this._state.searchString.indexOf('\n') >= 0) { + dom.addClass(this._domNode, 'multipleline'); + } else { + dom.removeClass(this._domNode, 'multipleline'); + } + try { this._ignoreChangeEvent = true; this._findInput.setValue(this._state.searchString); @@ -273,7 +304,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._updateButtons(); } if (e.replaceString) { - this._replaceInputBox.value = this._state.replaceString; + this._replaceInput.inputBox.value = this._state.replaceString; } if (e.isRevealed) { if (this._state.isRevealed) { @@ -286,8 +317,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas if (this._state.isReplaceRevealed) { if (!this._codeEditor.getConfiguration().readOnly && !this._isReplaceVisible) { this._isReplaceVisible = true; - this._replaceInputBox.width = this._findInput.inputBox.width; + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); this._updateButtons(); + this._replaceInput.inputBox.layout(); } } else { if (this._isReplaceVisible) { @@ -296,6 +328,12 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } } } + if ((e.isRevealed || e.isReplaceRevealed) && (this._state.isRevealed || this._state.isReplaceRevealed)) { + if (this._tryUpdateHeight()) { + this._showViewZone(); + } + } + if (e.isRegex) { this._findInput.setRegex(this._state.isRegex); } @@ -337,7 +375,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._findInput.inputBox.addToHistory(); } if (this._state.replaceString) { - this._replaceInputBox.addToHistory(); + this._replaceInput.inputBox.addToHistory(); } } @@ -402,7 +440,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas private _updateButtons(): void { this._findInput.setEnabled(this._isVisible); - this._replaceInputBox.setEnabled(this._isVisible && this._isReplaceVisible); + this._replaceInput.setEnabled(this._isVisible && this._isReplaceVisible); this._updateToggleSelectionFindButton(); this._closeBtn.setEnabled(this._isVisible); @@ -512,12 +550,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } this._codeEditor.changeViewZones((accessor) => { - if (this._state.isReplaceRevealed) { - viewZone.heightInPx = FIND_REPLACE_AREA_HEIGHT; - } else { - viewZone.heightInPx = FIND_INPUT_AREA_HEIGHT; - } - + viewZone.heightInPx = this._getHeight(); this._viewZoneId = accessor.addZone(viewZone); // scroll top adjust to make sure the editor doesn't scroll when adding viewzone at the beginning. this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + viewZone.heightInPx); @@ -525,30 +558,47 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } private _showViewZone(adjustScroll: boolean = true) { - const viewZone = this._viewZone; - if (!this._isVisible || !viewZone) { + if (!this._isVisible) { return; } + const addExtraSpaceOnTop = this._codeEditor.getConfiguration().contribInfo.find.addExtraSpaceOnTop; + + if (!addExtraSpaceOnTop) { + return; + } + + if (this._viewZone === undefined) { + this._viewZone = new FindWidgetViewZone(0); + } + + const viewZone = this._viewZone; + this._codeEditor.changeViewZones((accessor) => { - let scrollAdjustment = FIND_INPUT_AREA_HEIGHT; - if (this._viewZoneId !== undefined) { - if (this._state.isReplaceRevealed) { - viewZone.heightInPx = FIND_REPLACE_AREA_HEIGHT; - scrollAdjustment = FIND_REPLACE_AREA_HEIGHT - FIND_INPUT_AREA_HEIGHT; - } else { - viewZone.heightInPx = FIND_INPUT_AREA_HEIGHT; - scrollAdjustment = FIND_INPUT_AREA_HEIGHT - FIND_REPLACE_AREA_HEIGHT; + // the view zone already exists, we need to update the height + const newHeight = this._getHeight(); + if (newHeight === viewZone.heightInPx) { + return; } - accessor.removeZone(this._viewZoneId); - } else { - viewZone.heightInPx = FIND_INPUT_AREA_HEIGHT; - } - this._viewZoneId = accessor.addZone(viewZone); - if (adjustScroll) { - this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + scrollAdjustment); + let scrollAdjustment = newHeight - viewZone.heightInPx; + viewZone.heightInPx = newHeight; + accessor.layoutZone(this._viewZoneId); + + if (adjustScroll) { + this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + scrollAdjustment); + } + + return; + } else { + const scrollAdjustment = this._getHeight(); + viewZone.heightInPx = scrollAdjustment; + this._viewZoneId = accessor.addZone(viewZone); + + if (adjustScroll) { + this._codeEditor.setScrollTop(this._codeEditor.getScrollTop() + scrollAdjustment); + } } }); } @@ -584,8 +634,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder), }; this._findInput.style(inputStyles); - this._replaceInputBox.style(inputStyles); - this._preserveCase.style(inputStyles); + this._replaceInput.style(inputStyles); } private _tryUpdateWidgetWidth() { @@ -615,7 +664,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas if (widgetWidth > FIND_WIDGET_INITIAL_WIDTH) { // as the widget is resized by users, we may need to change the max width of the widget as the editor width changes. this._domNode.style.maxWidth = `${editorWidth - 28 - minimapWidth - 15}px`; - this._replaceInputBox.inputElement.style.width = `${dom.getTotalWidth(this._findInput.inputBox.inputElement)}px`; + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); return; } } @@ -639,13 +688,47 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } if (this._resized) { - let findInputWidth = dom.getTotalWidth(this._findInput.inputBox.inputElement); + this._findInput.inputBox.layout(); + let findInputWidth = this._findInput.inputBox.width; if (findInputWidth > 0) { - this._replaceInputBox.inputElement.style.width = `${findInputWidth}px`; + this._replaceInput.width = findInputWidth; } } } + private _getHeight(): number { + let totalheight = 0; + + // find input margin top + totalheight += 4; + + // find input height + totalheight += this._findInput.inputBox.height + 2 /** input box border */; + + if (this._isReplaceVisible) { + // replace input margin + totalheight += 4; + + totalheight += this._replaceInput.inputBox.height + 2 /** input box border */; + } + + // margin bottom + totalheight += 4; + return totalheight; + } + + private _tryUpdateHeight(): boolean { + const totalHeight = this._getHeight(); + if (this._cachedHeight !== null && this._cachedHeight === totalHeight) { + return false; + } + + this._cachedHeight = totalHeight; + this._domNode.style.height = `${totalHeight}px`; + + return true; + } + // ----- Public public focusFindInput(): void { @@ -655,9 +738,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } public focusReplaceInput(): void { - this._replaceInputBox.select(); + this._replaceInput.select(); // Edge browser requires focus() in addition to select() - this._replaceInputBox.focus(); + this._replaceInput.focus(); } public highlightFindOptions(): void { @@ -692,22 +775,25 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } private _onFindInputKeyDown(e: IKeyboardEvent): void { + if (e.equals(KeyMod.WinCtrl | KeyCode.Enter)) { + const inputElement = this._findInput.inputBox.inputElement; + const start = inputElement.selectionStart; + const end = inputElement.selectionEnd; + const content = inputElement.value; - if (e.equals(KeyCode.Enter)) { - this._codeEditor.getAction(FIND_IDS.NextMatchFindAction).run().then(undefined, onUnexpectedError); - e.preventDefault(); - return; - } - - if (e.equals(KeyMod.Shift | KeyCode.Enter)) { - this._codeEditor.getAction(FIND_IDS.PreviousMatchFindAction).run().then(undefined, onUnexpectedError); - e.preventDefault(); - return; + if (start && end) { + const value = content.substr(0, start) + '\n' + content.substr(end); + this._findInput.inputBox.value = value; + inputElement.setSelectionRange(start + 1, start + 1); + this._findInput.inputBox.layout(); + e.preventDefault(); + return; + } } if (e.equals(KeyCode.Tab)) { if (this._isReplaceVisible) { - this._replaceInputBox.focus(); + this._replaceInput.focus(); } else { this._findInput.focusOnCaseSensitive(); } @@ -720,20 +806,43 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas e.preventDefault(); return; } + + if (e.equals(KeyCode.UpArrow)) { + return stopPropagationForMultiLineUpwards(e, this._findInput.getValue(), this._findInput.domNode.querySelector('textarea')); + } + + if (e.equals(KeyCode.DownArrow)) { + return stopPropagationForMultiLineDownwards(e, this._findInput.getValue(), this._findInput.domNode.querySelector('textarea')); + } } private _onReplaceInputKeyDown(e: IKeyboardEvent): void { + if (e.equals(KeyMod.WinCtrl | KeyCode.Enter)) { + if (platform.isWindows && platform.isNative && !this._ctrlEnterReplaceAllWarningPrompted) { + // this is the first time when users press Ctrl + Enter to replace all + this._notificationService.info( + nls.localize('ctrlEnter.keybindingChanged', + 'Ctrl+Enter now inserts line break instead of replacing all. You can modify the keybinding for editor.action.replaceAll to override this behavior.') + ); - if (e.equals(KeyCode.Enter)) { - this._controller.replace(); - e.preventDefault(); - return; - } + this._ctrlEnterReplaceAllWarningPrompted = true; + this._storageService.store(ctrlEnterReplaceAllWarningPromptedKey, true, StorageScope.GLOBAL); - if (e.equals(KeyMod.CtrlCmd | KeyCode.Enter)) { - this._controller.replaceAll(); - e.preventDefault(); - return; + } + + const inputElement = this._replaceInput.inputBox.inputElement; + const start = inputElement.selectionStart; + const end = inputElement.selectionEnd; + const content = inputElement.value; + + if (start && end) { + const value = content.substr(0, start) + '\n' + content.substr(end); + this._replaceInput.inputBox.value = value; + inputElement.setSelectionRange(start + 1, start + 1); + this._replaceInput.inputBox.layout(); + e.preventDefault(); + return; + } } if (e.equals(KeyCode.Tab)) { @@ -753,6 +862,14 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas e.preventDefault(); return; } + + if (e.equals(KeyCode.UpArrow)) { + return stopPropagationForMultiLineUpwards(e, this._replaceInput.inputBox.value, this._replaceInput.inputBox.element.querySelector('textarea')); + } + + if (e.equals(KeyCode.DownArrow)) { + return stopPropagationForMultiLineDownwards(e, this._replaceInput.inputBox.value, this._replaceInput.inputBox.element.querySelector('textarea')); + } } // ----- sash @@ -777,6 +894,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } private _buildDomNode(): void { + const flexibleHeight = true; + const flexibleWidth = true; // Find input this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewProvider, { width: FIND_INPUT_AREA_WIDTH, @@ -796,7 +915,10 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } catch (e) { return { content: e.message }; } - } + }, + flexibleHeight, + flexibleWidth, + flexibleMaxHeight: 118 }, this._contextKeyService, true)); this._findInput.setRegex(!!this._state.isRegex); this._findInput.setCaseSensitive(!!this._state.matchCase); @@ -818,11 +940,24 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._register(this._findInput.onCaseSensitiveKeyDown((e) => { if (e.equals(KeyMod.Shift | KeyCode.Tab)) { if (this._isReplaceVisible) { - this._replaceInputBox.focus(); + this._replaceInput.focus(); e.preventDefault(); } } })); + this._register(this._findInput.onRegexKeyDown((e) => { + if (e.equals(KeyCode.Tab)) { + if (this._isReplaceVisible) { + this._replaceInput.focusOnPreserve(); + e.preventDefault(); + } + } + })); + this._register(this._findInput.inputBox.onDidHeightChange((e) => { + if (this._tryUpdateHeight()) { + this._showViewZone(); + } + })); if (platform.isLinux) { this._register(this._findInput.onMouseDown((e) => this._onFindInputMouseDown(e))); } @@ -852,13 +987,16 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas let findPart = document.createElement('div'); findPart.className = 'find-part'; findPart.appendChild(this._findInput.domNode); - findPart.appendChild(this._matchesCount); - findPart.appendChild(this._prevBtn.domNode); - findPart.appendChild(this._nextBtn.domNode); + const actionsContainer = document.createElement('div'); + actionsContainer.className = 'find-actions'; + findPart.appendChild(actionsContainer); + actionsContainer.appendChild(this._matchesCount); + actionsContainer.appendChild(this._prevBtn.domNode); + actionsContainer.appendChild(this._nextBtn.domNode); // Toggle selection button this._toggleSelectionFind = this._register(new SimpleCheckbox({ - parent: findPart, + parent: actionsContainer, title: NLS_TOGGLE_SELECTION_FIND_TITLE + this._keybindingLabelFor(FIND_IDS.ToggleSearchScopeCommand), onChange: () => { if (this._toggleSelectionFind.checked) { @@ -898,34 +1036,45 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } })); - findPart.appendChild(this._closeBtn.domNode); + actionsContainer.appendChild(this._closeBtn.domNode); // Replace input - let replaceInput = document.createElement('div'); - replaceInput.className = 'replace-input'; - replaceInput.style.width = REPLACE_INPUT_AREA_WIDTH + 'px'; - this._replaceInputBox = this._register(new ContextScopedHistoryInputBox(replaceInput, undefined, { - ariaLabel: NLS_REPLACE_INPUT_LABEL, + this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, { + label: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, - history: [] - }, this._contextKeyService)); - - - this._register(dom.addStandardDisposableListener(this._replaceInputBox.inputElement, 'keydown', (e) => this._onReplaceInputKeyDown(e))); - this._register(this._replaceInputBox.onDidChange(() => { - this._state.change({ replaceString: this._replaceInputBox.value }, false); + history: [], + flexibleHeight, + flexibleWidth, + flexibleMaxHeight: 118 + }, this._contextKeyService, true)); + this._replaceInput.setPreserveCase(!!this._state.preserveCase); + this._register(this._replaceInput.onKeyDown((e) => this._onReplaceInputKeyDown(e))); + this._register(this._replaceInput.inputBox.onDidChange(() => { + this._state.change({ replaceString: this._replaceInput.inputBox.value }, false); })); - - this._preserveCase = this._register(new Checkbox({ - actionClassName: 'monaco-preserve-case', - title: NLS_PRESERVE_CASE_LABEL, - isChecked: false, + this._register(this._replaceInput.inputBox.onDidHeightChange((e) => { + if (this._isReplaceVisible && this._tryUpdateHeight()) { + this._showViewZone(); + } })); - this._preserveCase.checked = !!this._state.preserveCase; - this._register(this._preserveCase.onChange(viaKeyboard => { - if (!viaKeyboard) { - this._state.change({ preserveCase: !this._state.preserveCase }, false); - this._replaceInputBox.focus(); + this._register(this._replaceInput.onDidOptionChange(() => { + this._state.change({ + preserveCase: this._replaceInput.getPreserveCase() + }, true); + })); + this._register(this._replaceInput.onPreserveCaseKeyDown((e) => { + if (e.equals(KeyCode.Tab)) { + if (this._prevBtn.isEnabled()) { + this._prevBtn.focus(); + } else if (this._nextBtn.isEnabled()) { + this._nextBtn.focus(); + } else if (this._toggleSelectionFind.isEnabled()) { + this._toggleSelectionFind.focus(); + } else if (this._closeBtn.isEnabled()) { + this._closeBtn.focus(); + } + + e.preventDefault(); } })); @@ -953,17 +1102,16 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } })); - let controls = document.createElement('div'); - controls.className = 'controls'; - controls.style.display = 'block'; - controls.appendChild(this._preserveCase.domNode); - replaceInput.appendChild(controls); - let replacePart = document.createElement('div'); replacePart.className = 'replace-part'; - replacePart.appendChild(replaceInput); - replacePart.appendChild(this._replaceBtn.domNode); - replacePart.appendChild(this._replaceAllBtn.domNode); + replacePart.appendChild(this._replaceInput.domNode); + + const replaceActionsContainer = document.createElement('div'); + replaceActionsContainer.className = 'replace-actions'; + replacePart.appendChild(replaceActionsContainer); + + replaceActionsContainer.appendChild(this._replaceBtn.domNode); + replaceActionsContainer.appendChild(this._replaceAllBtn.domNode); // Toggle replace button this._toggleReplaceBtn = this._register(new SimpleButton({ @@ -972,7 +1120,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas onTrigger: () => { this._state.change({ isReplaceRevealed: !this._isReplaceVisible }, false); if (this._isReplaceVisible) { - this._replaceInputBox.width = this._findInput.inputBox.width; + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); + this._replaceInput.inputBox.layout(); } this._showViewZone(); } @@ -1015,9 +1164,13 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas return; } this._domNode.style.width = `${width}px`; + this._findInput.inputBox.width = inputBoxWidth; if (this._isReplaceVisible) { - this._replaceInputBox.width = inputBoxWidth; + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); } + + this._findInput.inputBox.layout(); + this._tryUpdateHeight(); })); } @@ -1077,6 +1230,10 @@ class SimpleCheckbox extends Widget { return this._domNode; } + public isEnabled(): boolean { + return (this._domNode.tabIndex >= 0); + } + public get checked(): boolean { return this._checkbox.checked; } @@ -1086,7 +1243,7 @@ class SimpleCheckbox extends Widget { } public focus(): void { - this._checkbox.focus(); + this._domNode.focus(); } private enable(): void { @@ -1245,4 +1402,11 @@ registerThemingParticipant((theme, collector) => { if (inputActiveBackground) { collector.addRule(`.monaco-editor .find-widget .monaco-checkbox .checkbox:checked + .label { background-color: ${inputActiveBackground.toString()}; }`); } + + // This rule is used to override the outline color for synthetic-focus find input. + const focusOutline = theme.getColor(focusBorder); + if (focusOutline) { + collector.addRule(`.monaco-workbench .monaco-editor .find-widget .monaco-inputbox.synthetic-focus { outline-color: ${focusOutline}; }`); + + } }); diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts b/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts index 257ed3836c..b89bf12a5d 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsModel.ts @@ -52,7 +52,7 @@ export class ParameterHintsModel extends Disposable { public readonly onChangedHints = this._onChangedHints.event; private readonly editor: ICodeEditor; - private enabled: boolean; + private triggerOnType = false; private _state: ParameterHintState.State = ParameterHintState.Default; private readonly _lastSignatureHelpResult = this._register(new MutableDisposable()); private triggerChars = new CharacterSet(); @@ -68,7 +68,6 @@ export class ParameterHintsModel extends Disposable { super(); this.editor = editor; - this.enabled = false; this.throttledDelayer = new Delayer(delay); @@ -242,7 +241,7 @@ export class ParameterHintsModel extends Disposable { } private onDidType(text: string) { - if (!this.enabled) { + if (!this.triggerOnType) { return; } @@ -272,9 +271,9 @@ export class ParameterHintsModel extends Disposable { } private onEditorConfigurationChange(): void { - this.enabled = this.editor.getConfiguration().contribInfo.parameterHints.enabled; + this.triggerOnType = this.editor.getConfiguration().contribInfo.parameterHints.enabled; - if (!this.enabled) { + if (!this.triggerOnType) { this.cancel(); } } @@ -283,4 +282,4 @@ export class ParameterHintsModel extends Disposable { this.cancel(true); super.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 9dcfd89867..86ec9f94fb 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -97,6 +97,10 @@ font-weight: bold; } +.monaco-editor .suggest-widget-deprecated span { + text-decoration: line-through; +} + /** Icon styles **/ .monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .header > .close, diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 835c9c6e46..94797e2281 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -30,7 +30,7 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; -import { CompletionItemKind, completionKindToCssClass } from 'vs/editor/common/modes'; +import { CompletionItemKind, completionKindToCssClass, CompletionItemKindModifier } from 'vs/editor/common/modes'; import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -38,6 +38,7 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileKind } from 'vs/platform/files/common/files'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { flatten } from 'vs/base/common/arrays'; const expandSuggestionDocsByDefault = false; @@ -60,7 +61,6 @@ export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget. export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: listFocusBackground, light: listFocusBackground, hc: listFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.')); export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.')); - const colorRegExp = /^(#([\da-f]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))$/i; function extractColor(item: CompletionItem, out: string[]): boolean { if (item.completion.label.match(colorRegExp)) { @@ -173,18 +173,18 @@ class Renderer implements IListRenderer } else if (suggestion.kind === CompletionItemKind.File && this._themeService.getIconTheme().hasFileIcons) { // special logic for 'file' completion items data.icon.className = 'icon hide'; - labelOptions.extraClasses = ([] as string[]).concat( + labelOptions.extraClasses = flatten([ getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.label }), FileKind.FILE), getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.detail }), FileKind.FILE) - ); + ]); } else if (suggestion.kind === CompletionItemKind.Folder && this._themeService.getIconTheme().hasFolderIcons) { // special logic for 'folder' completion items data.icon.className = 'icon hide'; - labelOptions.extraClasses = ([] as string[]).concat( + labelOptions.extraClasses = flatten([ getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.label }), FileKind.FOLDER), getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.detail }), FileKind.FOLDER) - ); + ]); } else { // normal icon data.icon.className = 'icon hide'; @@ -193,6 +193,10 @@ class Renderer implements IListRenderer ]; } + if (suggestion.kindModifier && suggestion.kindModifier & CompletionItemKindModifier.Deprecated) { + labelOptions.extraClasses = (labelOptions.extraClasses || []).concat(['suggest-widget-deprecated']); + } + data.iconLabel.setLabel(suggestion.label, undefined, labelOptions); data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, ''); diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index bfa5b7738f..56cf187a1c 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -38,9 +38,6 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { clearAllFontInfos } from 'vs/editor/browser/config/configuration'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IProductService } from 'vs/platform/product/common/product'; -import { IStorageService } from 'vs/platform/storage/common/storage'; type Omit = Pick>; @@ -54,13 +51,7 @@ function withAllStandaloneServices(domElement: H } if (!services.has(IOpenerService)) { - services.set(IOpenerService, new OpenerService( - services.get(ICodeEditorService), - services.get(ICommandService), - services.get(IStorageService), - services.get(IDialogService), - services.get(IProductService) - )); + services.set(IOpenerService, new OpenerService(services.get(ICodeEditorService), services.get(ICommandService))); } let result = callback(services); diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 385f8dcf67..809174a6ac 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -562,6 +562,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { // enums DocumentHighlightKind: standaloneEnums.DocumentHighlightKind, CompletionItemKind: standaloneEnums.CompletionItemKind, + CompletionItemKindModifier: standaloneEnums.CompletionItemKindModifier, CompletionItemInsertTextRule: standaloneEnums.CompletionItemInsertTextRule, SymbolKind: standaloneEnums.SymbolKind, IndentAction: standaloneEnums.IndentAction, diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 20d09d3ffc..da6c8a38d4 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -7,18 +7,13 @@ import { URI } from 'vs/base/common/uri'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands'; -import { deepClone } from 'vs/base/common/objects'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IProductService } from 'vs/platform/product/common/product'; -import { IStorageService } from 'vs/platform/storage/common/storage'; suite('OpenerService', function () { - const editorService = new TestCodeEditorService(); - let lastCommand: { id: string, args: any[] } | undefined; + let lastCommand: { id: string; args: any[] } | undefined; - const commandService = new class implements ICommandService { + const commandService = new (class implements ICommandService { _serviceBrand: any; onWillExecuteCommand = () => ({ dispose: () => { } }); onDidExecuteCommand = () => ({ dispose: () => { } }); @@ -26,79 +21,20 @@ suite('OpenerService', function () { lastCommand = { id, args }; return Promise.resolve(undefined); } - }; - - function getStorageService(trustedDomainsSetting: string[]) { - let _settings = deepClone(trustedDomainsSetting); - - return new class implements IStorageService { - get = () => JSON.stringify(_settings); - store = (key: string, val: string) => _settings = JSON.parse(val); - - // Don't care - _serviceBrand: any; - - onDidChangeStorage = () => ({ dispose: () => { } }); - onWillSaveState = () => ({ dispose: () => { } }); - - getBoolean = () => true; - getNumber = () => 0; - remove = () => { }; - logStorage = () => { }; - }; - } - - function getDialogService() { - return new class implements IDialogService { - _showInvoked = 0; - show = () => { - this._showInvoked++; - return Promise.resolve({} as any); - } - get confirmInvoked() { return this._showInvoked; } - - // Don't care - _serviceBrand: any; - confirm = () => { - return Promise.resolve({} as any); - } - }; - } - - function getProductService(): IProductService { - return new class { - nameShort: 'VS Code'; - - _serviceBrand: any; - } as IProductService; - } - + })(); setup(function () { lastCommand = undefined; }); test('delegate to editorService, scheme:///fff', function () { - const openerService = new OpenerService( - editorService, - NullCommandService, - getStorageService([]), - getDialogService(), - getProductService() - ); + const openerService = new OpenerService(editorService, NullCommandService); openerService.open(URI.parse('another:///somepath')); assert.equal(editorService.lastInput!.options!.selection, undefined); }); test('delegate to editorService, scheme:///fff#L123', function () { - - const openerService = new OpenerService( - editorService, - NullCommandService, - getStorageService([]), - getDialogService(), - getProductService() - ); + const openerService = new OpenerService(editorService, NullCommandService); openerService.open(URI.parse('file:///somepath#L23')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); @@ -120,14 +56,7 @@ suite('OpenerService', function () { }); test('delegate to editorService, scheme:///fff#123,123', function () { - - const openerService = new OpenerService( - editorService, - NullCommandService, - getStorageService([]), - getDialogService(), - getProductService() - ); + const openerService = new OpenerService(editorService, NullCommandService); openerService.open(URI.parse('file:///somepath#23')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); @@ -145,14 +74,7 @@ suite('OpenerService', function () { }); test('delegate to commandsService, command:someid', function () { - - const openerService = new OpenerService( - editorService, - commandService, - getStorageService([]), - getDialogService(), - getProductService() - ); + const openerService = new OpenerService(editorService, commandService); const id = `aCommand${Math.random()}`; CommandsRegistry.registerCommand(id, function () { }); @@ -173,69 +95,107 @@ suite('OpenerService', function () { assert.equal(lastCommand!.args[1], true); }); - test('links are protected by dialog.show', function () { - const dialogService = getDialogService(); - const openerService = new OpenerService( - editorService, - commandService, - getStorageService([]), - dialogService, - getProductService() - ); + test('links are protected by validators', async function () { + const openerService = new OpenerService(editorService, commandService); - openerService.open(URI.parse('https://www.microsoft.com')); - assert.equal(dialogService.confirmInvoked, 1); + openerService.registerValidator({ shouldOpen: () => Promise.resolve(false) }); + + const httpResult = await openerService.open(URI.parse('https://www.microsoft.com')); + const httpsResult = await openerService.open(URI.parse('https://www.microsoft.com')); + assert.equal(httpResult, false); + assert.equal(httpsResult, false); }); - test('links on the whitelisted domains can be opened without dialog.show', function () { - const dialogService = getDialogService(); - const openerService = new OpenerService( - editorService, - commandService, - getStorageService(['https://microsoft.com']), - dialogService, - getProductService() - ); + test('links validated by validators go to openers', async function () { + const openerService = new OpenerService(editorService, commandService); - openerService.open(URI.parse('https://microsoft.com')); - openerService.open(URI.parse('https://microsoft.com/')); - openerService.open(URI.parse('https://microsoft.com/en-us/')); - openerService.open(URI.parse('https://microsoft.com/en-us/?foo=bar')); - openerService.open(URI.parse('https://microsoft.com/en-us/?foo=bar#baz')); + openerService.registerValidator({ shouldOpen: () => Promise.resolve(true) }); - assert.equal(dialogService.confirmInvoked, 0); + let openCount = 0; + openerService.registerOpener({ + open: (resource: URI) => { + openCount++; + return Promise.resolve(true); + } + }); + + await openerService.open(URI.parse('http://microsoft.com')); + assert.equal(openCount, 1); + await openerService.open(URI.parse('https://microsoft.com')); + assert.equal(openCount, 2); }); - test('variations of links are protected by dialog confirmation', function () { - const dialogService = getDialogService(); - const openerService = new OpenerService( - editorService, - commandService, - getStorageService(['https://microsoft.com']), - dialogService, - getProductService() - ); + test('links validated by multiple validators', async function () { + const openerService = new OpenerService(editorService, commandService); - openerService.open(URI.parse('http://microsoft.com')); - openerService.open(URI.parse('https://www.microsoft.com')); + let v1 = 0; + openerService.registerValidator({ + shouldOpen: () => { + v1++; + return Promise.resolve(true); + } + }); - assert.equal(dialogService.confirmInvoked, 2); + let v2 = 0; + openerService.registerValidator({ + shouldOpen: () => { + v2++; + return Promise.resolve(true); + } + }); + + let openCount = 0; + openerService.registerOpener({ + open: (resource: URI) => { + openCount++; + return Promise.resolve(true); + } + }); + + await openerService.open(URI.parse('http://microsoft.com')); + assert.equal(openCount, 1); + assert.equal(v1, 1); + assert.equal(v2, 1); + await openerService.open(URI.parse('https://microsoft.com')); + assert.equal(openCount, 2); + assert.equal(v1, 2); + assert.equal(v2, 2); }); - test('* removes all link protection', function () { - const dialogService = getDialogService(); - const openerService = new OpenerService( - editorService, - commandService, - getStorageService(['*']), - dialogService, - getProductService() - ); + test('links invalidated by first validator do not continue validating', async function () { + const openerService = new OpenerService(editorService, commandService); - openerService.open(URI.parse('https://code.visualstudio.com/')); - openerService.open(URI.parse('https://www.microsoft.com')); - openerService.open(URI.parse('https://www.github.com')); + let v1 = 0; + openerService.registerValidator({ + shouldOpen: () => { + v1++; + return Promise.resolve(false); + } + }); - assert.equal(dialogService.confirmInvoked, 0); + let v2 = 0; + openerService.registerValidator({ + shouldOpen: () => { + v2++; + return Promise.resolve(true); + } + }); + + let openCount = 0; + openerService.registerOpener({ + open: (resource: URI) => { + openCount++; + return Promise.resolve(true); + } + }); + + await openerService.open(URI.parse('http://microsoft.com')); + assert.equal(openCount, 0); + assert.equal(v1, 1); + assert.equal(v2, 0); + await openerService.open(URI.parse('https://microsoft.com')); + assert.equal(openCount, 0); + assert.equal(v1, 2); + assert.equal(v2, 0); }); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e09632e0e9..9c17fcde9d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4790,6 +4790,10 @@ declare namespace monaco.languages { Snippet = 25 } + export enum CompletionItemKindModifier { + Deprecated = 1 + } + export enum CompletionItemInsertTextRule { /** * Adjust whitespace/indentation of multiline insert texts to @@ -4818,6 +4822,11 @@ declare namespace monaco.languages { * an icon is chosen by the editor. */ kind: CompletionItemKind; + /** + * A modifier to the `kind` which affect how the item + * is rendered, e.g. Deprecated is rendered with a strikeout + */ + kindModifier?: CompletionItemKindModifier; /** * A human-readable string with additional information * about this item, like type or symbol information. diff --git a/src/vs/platform/browser/contextScopedHistoryWidget.ts b/src/vs/platform/browser/contextScopedHistoryWidget.ts index 7a5c3bf9b3..cd48b6a876 100644 --- a/src/vs/platform/browser/contextScopedHistoryWidget.ts +++ b/src/vs/platform/browser/contextScopedHistoryWidget.ts @@ -10,6 +10,7 @@ import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ReplaceInput, IReplaceInputOptions } from 'vs/base/browser/ui/findinput/replaceInput'; export const HistoryNavigationWidgetContext = 'historyNavigationWidget'; export const HistoryNavigationEnablementContext = 'historyNavigationEnabled'; @@ -60,6 +61,16 @@ export class ContextScopedFindInput extends FindInput { super(container, contextViewProvider, showFindOptions, options); this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, { target: this.inputBox.element, historyNavigator: this.inputBox }).scopedContextKeyService); } +} + +export class ContextScopedReplaceInput extends ReplaceInput { + + constructor(container: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, options: IReplaceInputOptions, + @IContextKeyService contextKeyService: IContextKeyService, showReplaceOptions: boolean = false + ) { + super(container, contextViewProvider, showReplaceOptions, options); + this._register(createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService, { target: this.inputBox.element, historyNavigator: this.inputBox }).scopedContextKeyService); + } } diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 7aa52a1fc5..1b5680c4c4 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -76,16 +76,27 @@ export function collectWorkspaceStats(folder: string, filter: string[]): Promise return done(results); } + if (token.count > MAX_FILES) { + token.count += files.length; + token.maxReached = true; + return done(results); + } + let pending = files.length; if (pending === 0) { return done(results); } - for (const file of files) { - if (token.maxReached) { - return done(results); - } + let filesToRead = files; + if (token.count + files.length > MAX_FILES) { + token.maxReached = true; + pending = MAX_FILES - token.count; + filesToRead = files.slice(0, pending); + } + token.count += files.length; + + for (const file of filesToRead) { stat(join(dir, file), (err, stats) => { // Ignore files that can't be read if (err) { @@ -108,11 +119,6 @@ export function collectWorkspaceStats(folder: string, filter: string[]): Promise } } } else { - if (token.count >= MAX_FILES) { - token.maxReached = true; - } - - token.count++; results.push(file); if (--pending === 0) { diff --git a/src/vs/platform/log/common/fileLogService.ts b/src/vs/platform/log/common/fileLogService.ts new file mode 100644 index 0000000000..0301f322cf --- /dev/null +++ b/src/vs/platform/log/common/fileLogService.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILogService, LogLevel, AbstractLogService } from 'vs/platform/log/common/log'; +import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; +import { Queue } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; + +export class FileLogService extends AbstractLogService implements ILogService { + + _serviceBrand: any; + + private readonly queue: Queue; + + constructor( + private readonly name: string, + private readonly resource: URI, + level: LogLevel, + @IFileService private readonly fileService: IFileService + ) { + super(); + this.setLevel(level); + this.queue = this._register(new Queue()); + } + + trace(): void { + if (this.getLevel() <= LogLevel.Trace) { + this._log(LogLevel.Trace, this.format(arguments)); + } + } + + debug(): void { + if (this.getLevel() <= LogLevel.Debug) { + this._log(LogLevel.Debug, this.format(arguments)); + } + } + + info(): void { + if (this.getLevel() <= LogLevel.Info) { + this._log(LogLevel.Info, this.format(arguments)); + } + } + + warn(): void { + if (this.getLevel() <= LogLevel.Warning) { + this._log(LogLevel.Warning, this.format(arguments)); + } + } + + error(): void { + if (this.getLevel() <= LogLevel.Error) { + const arg = arguments[0]; + + if (arg instanceof Error) { + const array = Array.prototype.slice.call(arguments) as any[]; + array[0] = arg.stack; + this._log(LogLevel.Error, this.format(array)); + } else { + this._log(LogLevel.Error, this.format(arguments)); + } + } + } + + critical(): void { + if (this.getLevel() <= LogLevel.Critical) { + this._log(LogLevel.Critical, this.format(arguments)); + } + } + + flush(): Promise { + return this.queue.queue(() => Promise.resolve()); + } + + log(level: LogLevel, args: any[]): void { + this._log(level, this.format(args)); + } + + private _log(level: LogLevel, message: string): void { + this.queue.queue(async () => { + let content = await this.loadContent(); + content += `[${this.getCurrentTimestamp()}] [${this.name}] [${this.stringifyLogLevel(level)}] ${message}\n`; + await this.fileService.writeFile(this.resource, VSBuffer.fromString(content)); + }); + } + + private getCurrentTimestamp(): string { + const toTwoDigits = (v: number) => v < 10 ? `0${v}` : v; + const toThreeDigits = (v: number) => v < 10 ? `00${v}` : v < 100 ? `0${v}` : v; + const currentTime = new Date(); + return `${currentTime.getFullYear()}-${toTwoDigits(currentTime.getMonth() + 1)}-${toTwoDigits(currentTime.getDate())} ${toTwoDigits(currentTime.getHours())}:${toTwoDigits(currentTime.getMinutes())}:${toTwoDigits(currentTime.getSeconds())}.${toThreeDigits(currentTime.getMilliseconds())}`; + } + + private async loadContent(): Promise { + try { + const content = await this.fileService.readFile(this.resource); + return content.value.toString(); + } catch (e) { + return ''; + } + } + + private stringifyLogLevel(level: LogLevel): string { + switch (level) { + case LogLevel.Critical: return 'critical'; + case LogLevel.Debug: return 'debug'; + case LogLevel.Error: return 'error'; + case LogLevel.Info: return 'info'; + case LogLevel.Trace: return 'trace'; + case LogLevel.Warning: return 'warning'; + } + return ''; + } + + private format(args: any): string { + let result = ''; + + for (let i = 0; i < args.length; i++) { + let a = args[i]; + + if (typeof a === 'object') { + try { + a = JSON.stringify(a); + } catch (e) { } + } + + result += (i > 0 ? ' ' : '') + a; + } + + return result; + } +} diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 8fe4413305..e3abbd35c9 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -14,6 +14,10 @@ export interface IOpener { open(resource: URI, options?: { openExternal?: boolean }): Promise; } +export interface IValidator { + shouldOpen(resource: URI): Promise; +} + export interface IOpenerService { _serviceBrand: any; @@ -23,6 +27,12 @@ export interface IOpenerService { */ registerOpener(opener: IOpener): IDisposable; + /** + * Register a participant that can validate if the URI resource be opened. + * validators are run before openers. + */ + registerValidator(validator: IValidator): IDisposable; + /** * Opens a resource, like a webaddress, a document uri, or executes command. * @@ -36,5 +46,6 @@ export interface IOpenerService { export const NullOpenerService: IOpenerService = Object.freeze({ _serviceBrand: undefined, registerOpener() { return { dispose() { } }; }, + registerValidator() { return { dispose() { } }; }, open() { return Promise.resolve(false); }, }); diff --git a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index 5138f4a42d..2b55b2f827 100644 --- a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ResolvedAuthority, IRemoteAuthorityResolverService, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { RemoteAuthorities } from 'vs/base/common/network'; export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverService { @@ -15,13 +16,14 @@ export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverS resolveAuthority(authority: string): Promise { if (authority.indexOf(':') >= 0) { const pieces = authority.split(':'); - return Promise.resolve({ - authority: { authority, host: pieces[0], port: parseInt(pieces[1], 10) } - }); + return Promise.resolve(this._createResolvedAuthority(authority, pieces[0], parseInt(pieces[1], 10))); } - return Promise.resolve({ - authority: { authority, host: authority, port: 80 } - }); + return Promise.resolve(this._createResolvedAuthority(authority, authority, 80)); + } + + private _createResolvedAuthority(authority: string, host: string, port: number): ResolverResult { + RemoteAuthorities.set(authority, host, port); + return { authority: { authority, host, port } }; } clearResolvedAuthority(authority: string): void { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 8ba2173645..18924a8570 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1141,4 +1141,15 @@ declare module 'vscode' { } //#endregion + + //#region Deprecated support + + export interface CompletionItem { + /** + * Indicates if this item is deprecated. + */ + deprecated?: boolean; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 382acc6b55..0de8da8538 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -330,6 +330,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return { label: data.a, kind: data.b, + kindModifier: data.n ? modes.CompletionItemKindModifier.Deprecated : undefined, detail: data.c, documentation: data.d, sortText: data.e, @@ -366,7 +367,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }; if (supportsResolveDetails) { provider.resolveCompletionItem = (model, position, suggestion, token) => { - return this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion._id, token).then(result => { + return this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion._id!, token).then(result => { if (!result) { return suggestion; } diff --git a/src/vs/workbench/api/browser/mainThreadLogService.ts b/src/vs/workbench/api/browser/mainThreadLogService.ts index aecacfa2d6..e57d76c98d 100644 --- a/src/vs/workbench/api/browser/mainThreadLogService.ts +++ b/src/vs/workbench/api/browser/mainThreadLogService.ts @@ -3,20 +3,46 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ILogService } from 'vs/platform/log/common/log'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IExtHostContext, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IExtHostContext, ExtHostContext, MainThreadLogShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { UriComponents, URI } from 'vs/base/common/uri'; +import { FileLogService } from 'vs/platform/log/common/fileLogService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { basename } from 'vs/base/common/path'; -@extHostCustomer -export class MainThreadLogService extends Disposable { +@extHostNamedCustomer(MainContext.MainThreadLog) +export class MainThreadLogService implements MainThreadLogShape { + + private readonly _loggers = new Map(); + private readonly _logListener: IDisposable; constructor( extHostContext: IExtHostContext, - @ILogService logService: ILogService, + @ILogService private readonly _logService: ILogService, + @IInstantiationService private readonly _instaService: IInstantiationService, ) { - super(); - this._register(logService.onDidChangeLogLevel(level => extHostContext.getProxy(ExtHostContext.ExtHostLogService).$setLevel(level))); + const proxy = extHostContext.getProxy(ExtHostContext.ExtHostLogService); + this._logListener = _logService.onDidChangeLogLevel(level => { + proxy.$setLevel(level); + this._loggers.forEach(value => value.setLevel(level)); + }); } -} \ No newline at end of file + dispose(): void { + this._logListener.dispose(); + this._loggers.forEach(value => value.dispose()); + this._loggers.clear(); + } + + $log(file: UriComponents, level: LogLevel, message: any[]): void { + const uri = URI.revive(file); + let logger = this._loggers.get(uri.toString()); + if (!logger) { + logger = this._instaService.createInstance(FileLogService, basename(file.path), URI.revive(file), this._logService.getLevel()); + this._loggers.set(uri.toString(), logger); + } + logger.log(level, message); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index f8c9c75d35..93a76472a6 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -292,7 +292,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews viewStates[handle] = { visible: input === group.activeEditor, active: input === activeInput, - position: editorGroupToViewColumn(this._editorGroupService, group.id || 0), + position: editorGroupToViewColumn(this._editorGroupService, group.id), }; } } @@ -304,10 +304,6 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews } private onDidClickLink(handle: WebviewPanelHandle, link: URI): void { - if (!link) { - return; - } - const webview = this.getWebviewEditorInput(handle); if (this.isSupportedLink(webview, link)) { this._openerService.open(link); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 38a0ac4362..ae40dc7973 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -939,6 +939,7 @@ export interface ISuggestDataDto { k/* commitCharacters */?: string[]; l/* additionalTextEdits */?: ISingleEditOperation[]; m/* command */?: modes.Command; + n/* deprecated */?: boolean; // not-standard x?: ChainedCacheId; } @@ -1287,6 +1288,10 @@ export interface ExtHostLogServiceShape { $setLevel(level: LogLevel): void; } +export interface MainThreadLogShape { + $log(file: UriComponents, level: LogLevel, args: any[]): void; +} + export interface ExtHostOutputServiceShape { $setVisibleChannel(channelId: string | null): void; } @@ -1329,6 +1334,7 @@ export const MainContext = { MainThreadKeytar: createMainId('MainThreadKeytar'), MainThreadLanguageFeatures: createMainId('MainThreadLanguageFeatures'), MainThreadLanguages: createMainId('MainThreadLanguages'), + MainThreadLog: createMainId('MainThread'), MainThreadMessageService: createMainId('MainThreadMessageService'), MainThreadOutputService: createMainId('MainThreadOutputService'), MainThreadProgress: createMainId('MainThreadProgress'), diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index f95f582e03..ffc06816f2 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -736,6 +736,7 @@ class SuggestAdapter { k: item.commitCharacters, l: item.additionalTextEdits && item.additionalTextEdits.map(typeConvert.TextEdit.from), m: this._commands.toInternal(item.command, disposables), + n: item.deprecated }; // 'insertText'-logic diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index f6d890d679..a1a4c2f807 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -8,7 +8,6 @@ import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHost import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { endsWith, startsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; import { joinPath } from 'vs/base/common/resources'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; @@ -128,7 +127,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { const next = joinPath(parent, '..', ensureSuffix(mod, '.js')); moduleStack.push(next); const trap = ExportsTrap.Instance.add(next.toString()); - importScripts(asDomUri(next).toString(true)); + importScripts(next.toString(true)); moduleStack.pop(); return trap.claim(); @@ -139,7 +138,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { module = module.with({ path: ensureSuffix(module.path, '.js') }); moduleStack.push(module); const trap = ExportsTrap.Instance.add(module.toString()); - importScripts(asDomUri(module).toString(true)); + importScripts(module.toString(true)); moduleStack.pop(); return Promise.resolve(trap.claim()); @@ -153,16 +152,6 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } } -// 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/api/worker/extHostLogService.ts b/src/vs/workbench/api/worker/extHostLogService.ts index c8d9d26d5f..949e82ed26 100644 --- a/src/vs/workbench/api/worker/extHostLogService.ts +++ b/src/vs/workbench/api/worker/extHostLogService.ts @@ -4,24 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import { ILogService, LogLevel, AbstractLogService } from 'vs/platform/log/common/log'; -import { ExtHostLogServiceShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostLogServiceShape, MainThreadLogShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput'; -import * as vscode from 'vscode'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { joinPath } from 'vs/base/common/resources'; +import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions'; +import { UriComponents } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; export class ExtHostLogService extends AbstractLogService implements ILogService, ExtHostLogServiceShape { _serviceBrand: any; - private readonly _logChannel: vscode.OutputChannel; + private readonly _proxy: MainThreadLogShape; + private readonly _logFile: UriComponents; constructor( + @IExtHostRpcService rpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @IExtHostOutputService extHostOutputService: IExtHostOutputService ) { super(); + const logFile = joinPath(initData.logsLocation, `${ExtensionHostLogFileName}.log`); + this._proxy = rpc.getProxy(MainContext.MainThreadLog); + this._logFile = logFile.toJSON(); this.setLevel(initData.logLevel); - this._logChannel = extHostOutputService.createOutputChannel('Log (Worker Extension Host)'); + extHostOutputService.createOutputChannelFromLogFile(localize('name', "Worker Extension Host"), logFile); } $setLevel(level: LogLevel): void { @@ -30,55 +39,37 @@ export class ExtHostLogService extends AbstractLogService implements ILogService trace(_message: string, ..._args: any[]): void { if (this.getLevel() <= LogLevel.Trace) { - this._logChannel.appendLine(this._format(arguments)); + this._proxy.$log(this._logFile, LogLevel.Trace, Array.from(arguments)); } } debug(_message: string, ..._args: any[]): void { if (this.getLevel() <= LogLevel.Debug) { - this._logChannel.appendLine(this._format(arguments)); + this._proxy.$log(this._logFile, LogLevel.Debug, Array.from(arguments)); } } info(_message: string, ..._args: any[]): void { if (this.getLevel() <= LogLevel.Info) { - this._logChannel.appendLine(this._format(arguments)); + this._proxy.$log(this._logFile, LogLevel.Info, Array.from(arguments)); } } warn(_message: string, ..._args: any[]): void { if (this.getLevel() <= LogLevel.Warning) { - this._logChannel.appendLine(this._format(arguments)); + this._proxy.$log(this._logFile, LogLevel.Warning, Array.from(arguments)); } } error(_message: string | Error, ..._args: any[]): void { if (this.getLevel() <= LogLevel.Error) { - this._logChannel.appendLine(this._format(arguments)); + this._proxy.$log(this._logFile, LogLevel.Error, Array.from(arguments)); } } critical(_message: string | Error, ..._args: any[]): void { if (this.getLevel() <= LogLevel.Critical) { - this._logChannel.appendLine(String(arguments)); + this._proxy.$log(this._logFile, LogLevel.Critical, Array.from(arguments)); } } - - private _format(args: any): string { - let result = ''; - - for (let i = 0; i < args.length; i++) { - let a = args[i]; - - if (typeof a === 'object') { - try { - a = JSON.stringify(a); - } catch (e) { } - } - - result += (i > 0 ? ' ' : '') + a; - } - - return result; - } } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 7eea94e08e..7366d7a392 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -8,7 +8,6 @@ import { domContentLoaded, addDisposableListener, EventType, addClass } from 'vs import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService } from 'vs/platform/log/common/log'; import { Disposable } from 'vs/base/common/lifecycle'; -import { SimpleLogService } from 'vs/workbench/browser/web.simpleservices'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { Workbench } from 'vs/workbench/browser/workbench'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; @@ -42,6 +41,10 @@ import { getThemeTypeSelector, DARK, HIGH_CONTRAST, LIGHT } from 'vs/platform/th import { InMemoryUserDataProvider } from 'vs/workbench/services/userData/common/inMemoryUserDataProvider'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { StaticExtensionsService, IStaticExtensionsService } from 'vs/workbench/services/extensions/common/staticExtensions'; +import { BufferLogService } from 'vs/platform/log/common/bufferLog'; +import { FileLogService } from 'vs/platform/log/common/fileLogService'; +import { toLocalISOString } from 'vs/base/common/date'; +import { INDEXEDDB_LOG_SCHEME, IndexedDBLogProvider } from 'vs/workbench/services/log/browser/indexedDBLogProvider'; class CodeRendererMain extends Disposable { @@ -117,13 +120,14 @@ class CodeRendererMain extends Disposable { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // Log - const logService = new SimpleLogService(); + const logsPath = URI.file(toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')).with({ scheme: INDEXEDDB_LOG_SCHEME }); + const logService = new BufferLogService(); serviceCollection.set(ILogService, logService); const payload = await this.resolveWorkspaceInitializationPayload(); // Environment - const environmentService = new BrowserWorkbenchEnvironmentService(payload.id, this.configuration); + const environmentService = new BrowserWorkbenchEnvironmentService({ workspaceId: payload.id, logsPath, ...this.configuration }); serviceCollection.set(IWorkbenchEnvironmentService, environmentService); // Product @@ -146,6 +150,10 @@ class CodeRendererMain extends Disposable { const fileService = this._register(new FileService(logService)); serviceCollection.set(IFileService, fileService); + // Logger + fileService.registerProvider(INDEXEDDB_LOG_SCHEME, new IndexedDBLogProvider()); + logService.logger = new FileLogService('window', environmentService.logFile, logService.getLevel(), fileService); + // Static Extensions const staticExtensions = new StaticExtensionsService(this.configuration.staticExtensions || []); serviceCollection.set(IStaticExtensionsService, staticExtensions); diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts index 79a5aec27e..2eb195d725 100644 --- a/src/vs/workbench/browser/web.simpleservices.ts +++ b/src/vs/workbench/browser/web.simpleservices.ts @@ -8,10 +8,9 @@ import * as browser from 'vs/base/browser/browser'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionTipsService, ExtensionRecommendationReason, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IURLHandler, IURLService } from 'vs/platform/url/common/url'; -import { ConsoleLogService, ILogService } from 'vs/platform/log/common/log'; +import { ILogService } from 'vs/platform/log/common/log'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IUpdateService, State } from 'vs/platform/update/common/update'; @@ -31,7 +30,6 @@ 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'; -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'; @@ -40,55 +38,6 @@ 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 - -export class SimpleExtensionTipsService implements IExtensionTipsService { - _serviceBrand: any; - - onRecommendationChange = Event.None; - - getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string; }; } { - return Object.create(null); - } - - getFileBasedRecommendations(): IExtensionRecommendation[] { - return []; - } - - getOtherRecommendations(): Promise { - return Promise.resolve([]); - } - - getWorkspaceRecommendations(): Promise { - return Promise.resolve([]); - } - - getKeymapRecommendations(): IExtensionRecommendation[] { - return []; - } - - toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void { - } - - getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } { - return { global: [], workspace: [] }; - } - - // {{SQL CARBON EDIT}} - getRecommendedExtensionsByScenario(scenarioType: string): Promise { - return Promise.resolve([]); - } - - promptRecommendedExtensionsByScenario(scenarioType: string): void { - return; - } - // {{SQL CARBON EDIT}} - End -} - -registerSingleton(IExtensionTipsService, SimpleExtensionTipsService, true); - -//#endregion - //#region Extension URL Handler export const IExtensionUrlHandler = createDecorator('inactiveExtensionUrlHandler'); @@ -112,12 +61,6 @@ registerSingleton(IExtensionUrlHandler, SimpleExtensionURLHandler, true); //#endregion -//#region Log - -export class SimpleLogService extends ConsoleLogService { } - -//#endregion - //#region Update export class SimpleUpdateService implements IUpdateService { @@ -192,7 +135,6 @@ export class SimpleWindowService extends Disposable implements IWindowService { @IStorageService private readonly storageService: IStorageService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @ILogService private readonly logService: ILogService, - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { super(); @@ -388,7 +330,7 @@ export class SimpleWindowService extends Disposable implements IWindowService { for (let i = 0; i < _uris.length; i++) { const uri = _uris[i]; if ('folderUri' in uri) { - const newAddress = `${document.location.origin}/?folder=${uri.folderUri.path}${this.workbenchEnvironmentService.configuration.connectionToken ? `&tkn=${this.workbenchEnvironmentService.configuration.connectionToken}` : ''}`; + const newAddress = `${document.location.origin}/?folder=${uri.folderUri.path}`; if (openFolderInNewWindow) { window.open(newAddress); } else { @@ -475,7 +417,6 @@ export class SimpleWindowsService implements IWindowsService { readonly onRecentlyOpenedChange: Event = Event.None; constructor( - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @IDialogService private readonly dialogService: IDialogService, @IProductService private readonly productService: IProductService, @IClipboardService private readonly clipboardService: IClipboardService @@ -667,11 +608,6 @@ export class SimpleWindowsService implements IWindowsService { addQueryParameter('ibe', ibe); } - // add connection token - if (this.workbenchEnvironmentService.configuration.connectionToken) { - addQueryParameter('tkn', this.workbenchEnvironmentService.configuration.connectionToken); - } - window.open(newAddress); return Promise.resolve(); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 222b389431..2a63c430c5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -9,7 +9,7 @@ import * as dom from 'vs/base/browser/dom'; import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; import { Widget } from 'vs/base/browser/ui/widget'; import { Delayer } from 'vs/base/common/async'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { SimpleButton } from 'vs/editor/contrib/find/findWidget'; @@ -42,8 +42,7 @@ export abstract class SimpleFindWidget extends Widget { @IContextViewService private readonly _contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, private readonly _state: FindReplaceState = new FindReplaceState(), - showOptionButtons?: boolean, - private readonly _invertDefaultDirection: boolean = false + showOptionButtons?: boolean ) { super(); @@ -94,20 +93,6 @@ export abstract class SimpleFindWidget extends Widget { this.findFirst(); })); - this._register(this._findInput.onKeyDown((e) => { - if (e.equals(KeyCode.Enter)) { - this.find(this._invertDefaultDirection); - e.preventDefault(); - return; - } - - if (e.equals(KeyMod.Shift | KeyCode.Enter)) { - this.find(!this._invertDefaultDirection); - e.preventDefault(); - return; - } - })); - this.prevBtn = this._register(new SimpleButton({ label: NLS_PREVIOUS_MATCH_BTN_LABEL, className: 'previous', diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index a1fb2c3511..088ae604b7 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -593,13 +593,13 @@ class CallStackDataSource implements IAsyncDataSource { + async getChildren(element: IDebugModel | CallStackItem): Promise { if (isDebugModel(element)) { const sessions = element.getSessions(); if (sessions.length === 0) { return Promise.resolve([]); } - if (sessions.length > 1 || this.debugService.getViewModel().isMultiSessionView()) { + if (sessions.length > 1) { return Promise.resolve(sessions.filter(s => !s.parentSession)); } @@ -609,9 +609,10 @@ class CallStackDataSource implements IAsyncDataSource s.parentSession === element); const threads: CallStackItem[] = element.getAllThreads(); - if (threads.length === 1 && childSessions.length === 0) { + if (threads.length === 1) { // Do not show thread when there is only one to be compact. - return this.getThreadChildren(threads[0]); + const children = await this.getThreadChildren(threads[0]); + return children.concat(childSessions); } return Promise.resolve(threads.concat(childSessions)); diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index e5318c2b78..ba896e2c0c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -245,6 +245,9 @@ export class DebugHoverWidget implements IContentWidget { this.layoutTreeAndContainer(); this.editor.layoutContentWidget(this); this.scrollbar.scanDomNode(); + this.tree.scrollTop = 0; + this.tree.scrollLeft = 0; + if (focus) { this.editor.render(); this.tree.domFocus(); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index d7b0ff88b9..f999a02033 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -479,11 +479,11 @@ export class DebugService implements IDebugService { }); } - private launchOrAttachToSession(session: IDebugSession, focus = true): Promise { + private launchOrAttachToSession(session: IDebugSession, forceFocus = false): Promise { const dbgr = this.configurationManager.getDebugger(session.configuration.type); return session.initialize(dbgr!).then(() => { return session.launchOrAttach(session.configuration).then(() => { - if (focus) { + if (forceFocus || !this.viewModel.focusedSession) { this.focusStackFrame(undefined, undefined, session); } }); @@ -572,7 +572,7 @@ export class DebugService implements IDebugService { return runTasks().then(taskResult => taskResult === TaskRunResult.Success ? this.extensionHostDebugService.reload(session.getId()) : undefined); } - const shouldFocus = this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId(); + const shouldFocus = !!this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId(); // If the restart is automatic -> disconnect, otherwise -> terminate #55064 return (isAutoRestart ? session.disconnect(true) : session.terminate(true)).then(() => { diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 38285884a0..bc82617dd8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -131,6 +131,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { private registerListeners(): void { this._register(this.debugService.onDidChangeState(() => this.updateScheduler.schedule())); this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateScheduler.schedule())); + this._register(this.debugService.onDidNewSession(() => this.updateScheduler.schedule())); this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidConfigurationChange(e))); this._register(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => { // check for error diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index fd5dbed24f..365a0cead0 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -50,7 +50,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry'; import { isUndefined } from 'vs/base/common/types'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IWebviewService, Webview } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, Webview, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderDashboardContributions } from 'sql/workbench/parts/extensions/browser/contributionRenders'; // {{SQL CARBON EDIT}} import { generateUuid } from 'vs/base/common/uuid'; @@ -536,6 +536,12 @@ export class ExtensionEditor extends BaseEditor { } } + runFindAction(previous: boolean): void { + if (this.activeElement && (this.activeElement).runFindAction) { + (this.activeElement).runFindAction(previous); + } + } + private onNavbarChange(extension: IExtension, { id, focus }: { id: string | null, focus: boolean }, template: IExtensionEditorTemplate): void { if (this.editorLoadComplete) { /* __GDPR__ @@ -1326,28 +1332,66 @@ export class ExtensionEditor extends BaseEditor { } } +const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', ExtensionEditor.ID), ContextKeyExpr.not('editorFocus')); class ShowExtensionEditorFindCommand extends Command { public runCommand(accessor: ServicesAccessor, args: any): void { - const extensionEditor = this.getExtensionEditor(accessor); + const extensionEditor = getExtensionEditor(accessor); if (extensionEditor) { extensionEditor.showFind(); } } - - private getExtensionEditor(accessor: ServicesAccessor): ExtensionEditor | null { - const activeControl = accessor.get(IEditorService).activeControl as ExtensionEditor; - if (activeControl instanceof ExtensionEditor) { - return activeControl; - } - return null; - } } -const showCommand = new ShowExtensionEditorFindCommand({ +(new ShowExtensionEditorFindCommand({ id: 'editor.action.extensioneditor.showfind', - precondition: ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', ExtensionEditor.ID), ContextKeyExpr.not('editorFocus')), + precondition: contextKeyExpr, kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.KEY_F, weight: KeybindingWeight.EditorContrib } -}); -showCommand.register(); +})).register(); + +class StartExtensionEditorFindNextCommand extends Command { + public runCommand(accessor: ServicesAccessor, args: any): void { + const extensionEditor = getExtensionEditor(accessor); + if (extensionEditor) { + extensionEditor.runFindAction(false); + } + } +} +(new StartExtensionEditorFindNextCommand({ + id: 'editor.action.extensioneditor.findNext', + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED), + kbOpts: { + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } +})).register(); + +class StartExtensionEditorFindPreviousCommand extends Command { + public runCommand(accessor: ServicesAccessor, args: any): void { + const extensionEditor = getExtensionEditor(accessor); + if (extensionEditor) { + extensionEditor.runFindAction(true); + } + } +} +(new StartExtensionEditorFindPreviousCommand({ + id: 'editor.action.extensioneditor.findPrevious', + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED), + kbOpts: { + primary: KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } +})).register(); + +function getExtensionEditor(accessor: ServicesAccessor): ExtensionEditor | null { + const activeControl = accessor.get(IEditorService).activeControl as ExtensionEditor; + if (activeControl instanceof ExtensionEditor) { + return activeControl; + } + return null; +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts deleted file mode 100644 index e749720db6..0000000000 --- a/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/browser/extensionTipsService'; - -// Singletons -registerSingleton(IExtensionTipsService, ExtensionTipsService); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index c03daece76..d138db2745 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -3100,7 +3100,6 @@ 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, @@ -3122,9 +3121,7 @@ export class InstallLocalExtensionsInRemoteAction extends Action { get label(): string { 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 localize('select and install local extensions', "Install Local Extensions in {0}...", this.extensionManagementServerService.remoteExtensionManagementServer.label); } return ''; } @@ -3140,12 +3137,7 @@ export class InstallLocalExtensionsInRemoteAction extends Action { } async run(): Promise { - if (this.selectAndInstall) { - return this.selectAndInstallLocalExtensions(); - } else { - const extensionsToInstall = await this.queryExtensionsToInstall(); - return this.installLocalExtensions(extensionsToInstall); - } + return this.selectAndInstallLocalExtensions(); } private async queryExtensionsToInstall(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 2914fce186..5b9730d7ea 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -961,7 +961,7 @@ export class ServerExtensionsView extends ExtensionsListView { getActions(): IAction[] { if (this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === this.server) { - const installLocalExtensionsInRemoteAction = this._register(this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction, false)); + const installLocalExtensionsInRemoteAction = this._register(this.instantiationService.createInstance(InstallLocalExtensionsInRemoteAction)); installLocalExtensionsInRemoteAction.class = 'octicon octicon-cloud-download'; return [installLocalExtensionsInRemoteAction]; } diff --git a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts index c750591e01..44595ecaf9 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, true); + const installLocalExtensionsInRemoteAction = instantiationService.createInstance(InstallLocalExtensionsInRemoteAction); CommandsRegistry.registerCommand('workbench.extensions.installLocalExtensions', () => installLocalExtensionsInRemoteAction.run()); let disposable = Disposable.None; const appendMenuItem = () => { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts index c39f926a85..3d3b3e011a 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts @@ -11,6 +11,7 @@ import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/dec import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { explorerRootErrorEmitter } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; export class ExplorerDecorationsProvider implements IDecorationsProvider { readonly label: string = localize('label', "Explorer"); @@ -30,6 +31,9 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { this._onDidChange.fire([change.item.resource]); } })); + this.toDispose.add(explorerRootErrorEmitter.event((resource => { + this._onDidChange.fire([resource]); + }))); } get onDidChange(): Event { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 9af8310636..a14c80c605 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -50,6 +50,7 @@ import { first } from 'vs/base/common/arrays'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { dispose } from 'vs/base/common/lifecycle'; +import { timeout } from 'vs/base/common/async'; export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; @@ -522,6 +523,8 @@ export class ExplorerView extends ViewletPanel { while (item && item.resource.toString() !== resource.toString()) { await this.tree.expand(item); + // Tree returns too early from the expand, need to wait for next tick #77106 + await timeout(0); item = first(values(item.children), i => isEqualOrParent(resource, i.resource)); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 5a2b4a6fd3..c6bbc1018b 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -46,6 +46,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { Emitter } from 'vs/base/common/event'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -60,6 +61,7 @@ export class ExplorerDelegate implements IListVirtualDelegate { } } +export const explorerRootErrorEmitter = new Emitter(); export class ExplorerDataSource implements IAsyncDataSource { constructor( @@ -87,8 +89,9 @@ export class ExplorerDataSource implements IAsyncDataSource(WorkbenchActionExtensions.WorkbenchActions); const devCategory = nls.localize('developer', "Developer"); -workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.LABEL), 'Developer: Set Log Level...', devCategory); \ No newline at end of file +workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(SetLogLevelAction, SetLogLevelAction.ID, SetLogLevelAction.LABEL), 'Developer: Set Log Level...', devCategory); + +class LogOutputChannels extends Disposable implements IWorkbenchContribution { + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + if (isWeb) { + this.registerWebContributions(); + } else { + this.registerNativeContributions(); + } + } + + private registerWebContributions(): void { + Registry.as(OutputExt.OutputChannels).registerChannel({ id: Constants.rendererLogChannelId, label: nls.localize('rendererLog', "Window"), file: this.environmentService.logFile, log: true }); + this.instantiationService.createInstance(LogsDataCleaner); + + const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); + const devCategory = nls.localize('developer', "Developer"); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenSessionLogFileAction, OpenSessionLogFileAction.ID, OpenSessionLogFileAction.LABEL), 'Developer: Open Log File (Session)...', devCategory); + } + + private registerNativeContributions(): void { + this.registerLogChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Main"), URI.file(join(this.environmentService.logsPath, `main.log`))); + this.registerLogChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Shared"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`))); + this.registerLogChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Window"), this.environmentService.logFile); + + const registerTelemetryChannel = (level: LogLevel) => { + if (level === LogLevel.Trace && !Registry.as(OutputExt.OutputChannels).getChannel(Constants.telemetryLogChannelId)) { + this.registerLogChannel(Constants.telemetryLogChannelId, nls.localize('telemetryLog', "Telemetry"), URI.file(join(this.environmentService.logsPath, `telemetry.log`))); + } + }; + registerTelemetryChannel(this.logService.getLevel()); + this.logService.onDidChangeLogLevel(registerTelemetryChannel); + + const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); + const devCategory = nls.localize('developer', "Developer"); + workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenLogsFolderAction, OpenLogsFolderAction.ID, OpenLogsFolderAction.LABEL), 'Developer: Open Logs Folder', devCategory); + } + + private async registerLogChannel(id: string, label: string, file: URI): Promise { + const outputChannelRegistry = Registry.as(OutputExt.OutputChannels); + const exists = await this.fileService.exists(file); + if (exists) { + outputChannelRegistry.registerChannel({ id, label, file, log: true }); + return; + } + + const watcher = this.fileService.watch(dirname(file)); + const disposable = this.fileService.onFileChanges(e => { + if (e.contains(file, FileChangeType.ADDED)) { + watcher.dispose(); + disposable.dispose(); + outputChannelRegistry.registerChannel({ id, label, file, log: true }); + } + }); + } + +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 7e5b080fee..c3e0cc7618 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -9,8 +9,12 @@ import { join } from 'vs/base/common/path'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWindowsService } from 'vs/platform/windows/common/windows'; import { ILogService, LogLevel, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { dirname, basename, isEqual } from 'vs/base/common/resources'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class OpenLogsFolderAction extends Action { @@ -73,3 +77,68 @@ export class SetLogLevelAction extends Action { return undefined; } } + +export class OpenSessionLogFileAction extends Action { + + static ID = 'workbench.action.openSessionLogFile'; + static LABEL = nls.localize('openSessionLogFile', "Open Log File (Session)..."); + + constructor(id: string, label: string, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IFileService private readonly fileService: IFileService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IEditorService private readonly editorService: IEditorService, + ) { + super(id, label); + } + + async run(): Promise { + const sessionResult = await this.quickInputService.pick( + this.getSessions().then(sessions => sessions.map((s, index) => ({ + id: s.toString(), + label: basename(s), + description: index === 0 ? nls.localize('current', "Current") : undefined + }))), + { + canPickMany: false, + placeHolder: nls.localize('sessions placeholder', "Select Session") + }); + if (sessionResult) { + const logFileResult = await this.quickInputService.pick( + this.getLogFiles(URI.parse(sessionResult.id!)).then(logFiles => logFiles.map(s => ({ + id: s.toString(), + label: basename(s) + }))), + { + canPickMany: false, + placeHolder: nls.localize('log placeholder', "Select Log file") + }); + if (logFileResult) { + return this.editorService.openEditor({ resource: URI.parse(logFileResult.id!) }).then(() => undefined); + } + } + } + + private async getSessions(): Promise { + const logsPath = URI.file(this.environmentService.logsPath).with({ scheme: this.environmentService.logFile.scheme }); + const result: URI[] = [logsPath]; + const stat = await this.fileService.resolve(dirname(logsPath)); + if (stat.children) { + result.push(...stat.children + .filter(stat => !isEqual(stat.resource, logsPath) && stat.isDirectory && /^\d{8}T\d{6}$/.test(stat.name)) + .sort() + .reverse() + .map(d => d.resource)); + } + return result; + } + + private async getLogFiles(session: URI): Promise { + const stat = await this.fileService.resolve(session); + if (stat.children) { + return stat.children.filter(stat => !stat.isDirectory).map(stat => stat.resource); + } + return []; + } +} + diff --git a/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts b/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts new file mode 100644 index 0000000000..c49b48da75 --- /dev/null +++ b/src/vs/workbench/contrib/logs/common/logsDataCleaner.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { basename, dirname } from 'vs/base/common/resources'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { URI } from 'vs/base/common/uri'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; + +export class LogsDataCleaner extends Disposable { + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IFileService private readonly fileService: IFileService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + ) { + super(); + this.cleanUpOldLogsSoon(); + } + + private cleanUpOldLogsSoon(): void { + let handle: NodeJS.Timeout | undefined = setTimeout(async () => { + handle = undefined; + const logsPath = URI.file(this.environmentService.logsPath).with({ scheme: this.environmentService.logFile.scheme }); + const stat = await this.fileService.resolve(dirname(logsPath)); + if (stat.children) { + const currentLog = basename(logsPath); + const allSessions = stat.children.filter(stat => stat.isDirectory && /^\d{8}T\d{6}$/.test(stat.name)); + const oldSessions = allSessions.sort().filter((d, i) => d.name !== currentLog); + const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 49)); + Promise.all(toDelete.map(stat => this.fileService.del(stat.resource, { recursive: true }))); + } + }, 10 * 1000); + this.lifecycleService.onWillShutdown(() => { + if (handle) { + clearTimeout(handle); + handle = undefined; + } + }); + } +} diff --git a/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts b/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts deleted file mode 100644 index 92b66a1344..0000000000 --- a/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import { join } from 'vs/base/common/path'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IOutputChannelRegistry, Extensions as OutputExt, } from 'vs/workbench/contrib/output/common/output'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { OpenLogsFolderAction } from 'vs/workbench/contrib/logs/common/logsActions'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; -import { dirname } from 'vs/base/common/resources'; - -class LogOutputChannels extends Disposable implements IWorkbenchContribution { - - constructor( - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @ILogService logService: ILogService, - @IFileService private readonly fileService: IFileService - ) { - super(); - this.registerLogChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Main"), URI.file(join(environmentService.logsPath, `main.log`))); - this.registerLogChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Shared"), URI.file(join(environmentService.logsPath, `sharedprocess.log`))); - this.registerLogChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Window"), URI.file(join(environmentService.logsPath, `renderer${environmentService.configuration.windowId}.log`))); - - const registerTelemetryChannel = (level: LogLevel) => { - if (level === LogLevel.Trace && !Registry.as(OutputExt.OutputChannels).getChannel(Constants.telemetryLogChannelId)) { - this.registerLogChannel(Constants.telemetryLogChannelId, nls.localize('telemetryLog', "Telemetry"), URI.file(join(environmentService.logsPath, `telemetry.log`))); - } - }; - registerTelemetryChannel(logService.getLevel()); - logService.onDidChangeLogLevel(registerTelemetryChannel); - - const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); - const devCategory = nls.localize('developer', "Developer"); - workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(OpenLogsFolderAction, OpenLogsFolderAction.ID, OpenLogsFolderAction.LABEL), 'Developer: Open Logs Folder', devCategory); - } - - private async registerLogChannel(id: string, label: string, file: URI): Promise { - const outputChannelRegistry = Registry.as(OutputExt.OutputChannels); - const exists = await this.fileService.exists(file); - if (exists) { - outputChannelRegistry.registerChannel({ id, label, file, log: true }); - return; - } - - const watcher = this.fileService.watch(dirname(file)); - const disposable = this.fileService.onFileChanges(e => { - if (e.contains(file, FileChangeType.ADDED)) { - watcher.dispose(); - disposable.dispose(); - outputChannelRegistry.registerChannel({ id, label, file, log: true }); - } - }); - } - -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 4004d0ea1b..6aac8f1822 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -109,7 +109,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor ) { super(KeybindingsEditor.ID, telemetryService, themeService, storageService); this.delayedFiltering = new Delayer(300); - this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(true))); + this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(!!this.keybindingFocusContextKey.get()))); this.keybindingsEditorContextKey = CONTEXT_KEYBINDINGS_EDITOR.bindTo(this.contextKeyService); this.searchFocusContextKey = CONTEXT_KEYBINDINGS_SEARCH_FOCUS.bindTo(this.contextKeyService); @@ -537,7 +537,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor } this.unAssignedKeybindingItemToRevealAndFocus = null; } else if (currentSelectedIndex !== -1 && currentSelectedIndex < this.listEntries.length) { - this.selectEntry(currentSelectedIndex); + this.selectEntry(currentSelectedIndex, preserveFocus); } else if (this.editorService.activeControl === this && !preserveFocus) { this.focus(); } @@ -597,11 +597,13 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor return -1; } - private selectEntry(keybindingItemEntry: IKeybindingItemEntry | number): void { + private selectEntry(keybindingItemEntry: IKeybindingItemEntry | number, focus: boolean = true): void { const index = typeof keybindingItemEntry === 'number' ? keybindingItemEntry : this.getIndexOf(keybindingItemEntry); if (index !== -1) { - this.keybindingsList.getHTMLElement().focus(); - this.keybindingsList.setFocus([index]); + if (focus) { + this.keybindingsList.getHTMLElement().focus(); + this.keybindingsList.setFocus([index]); + } this.keybindingsList.setSelection([index]); } } diff --git a/src/vs/workbench/contrib/preferences/browser/tocTree.ts b/src/vs/workbench/contrib/preferences/browser/tocTree.ts index 45de9c93aa..4e5d55d961 100644 --- a/src/vs/workbench/contrib/preferences/browser/tocTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/tocTree.ts @@ -115,6 +115,7 @@ export class TOCRenderer implements ITreeRenderer { //#region --- fetching/caching const _cacheName = 'vscode-extension-resources'; -const _resourcePrefix = '/vscode-remote'; +const _resourcePrefix = '/vscode-remote-resource'; const _pendingFetch = new Map(); self.addEventListener('message', event => { diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts index 9c008dbad7..fc2b4ade1d 100644 --- a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts @@ -10,7 +10,7 @@ // statement. // trigger service worker updates -const _tag = '52278406-3ca9-48af-a8fb-8495add5bb4e'; +const _tag = '23549971-9b8d-41bb-92ae-d7f6a68c9702'; // loader world const baseUrl = '../../../../../'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index bc7b49f930..8b9bd012d5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -530,7 +530,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindNext, FindNe }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Find next', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindNext, FindNext.ID, FindNext.LABEL, { primary: KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] } + mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3, KeyMod.Shift | KeyCode.Enter] } }, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find next'); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindPrevious, FindPrevious.ID_TERMINAL_FOCUS, FindPrevious.LABEL, { primary: KeyMod.Shift | KeyCode.F3, @@ -538,7 +538,7 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindPrevious, Fi }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Find previous', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FindPrevious, FindPrevious.ID, FindPrevious.LABEL, { primary: KeyMod.Shift | KeyCode.F3, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] }, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3, KeyCode.Enter] }, }, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find previous'); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index bb414db506..f5944e3c0a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -19,7 +19,7 @@ export class TerminalFindWidget extends SimpleFindWidget { @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ITerminalService private readonly _terminalService: ITerminalService ) { - super(_contextViewService, _contextKeyService, findState, true, true); + super(_contextViewService, _contextKeyService, findState, true); this._register(findState.onFindReplaceStateChange(() => { this.show(); })); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 6b1668685c..6fa5c1d18e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -26,6 +26,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur export class TerminalService extends CommonTerminalService implements ITerminalService { private _configHelper: IBrowserTerminalConfigHelper; + private _terminalContainer: HTMLElement | undefined; public get configHelper(): ITerminalConfigHelper { return this._configHelper; } diff --git a/src/vs/workbench/contrib/terminal/common/terminalService.ts b/src/vs/workbench/contrib/terminal/common/terminalService.ts index 2ffbf54bab..a06d3a6b1d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalService.ts @@ -35,7 +35,6 @@ export abstract class TerminalService implements ITerminalService { protected _isShuttingDown: boolean; protected _terminalFocusContextKey: IContextKey; protected _findWidgetVisible: IContextKey; - protected _terminalContainer: HTMLElement | undefined; protected _terminalTabs: ITerminalTab[] = []; protected _backgroundedTerminalInstances: ITerminalInstance[] = []; protected get _terminalInstances(): ITerminalInstance[] { diff --git a/src/vs/workbench/contrib/url/common/url.contribution.ts b/src/vs/workbench/contrib/url/common/url.contribution.ts index 62e6746c55..e743113254 100644 --- a/src/vs/workbench/contrib/url/common/url.contribution.ts +++ b/src/vs/workbench/contrib/url/common/url.contribution.ts @@ -14,17 +14,24 @@ import { Action } from 'vs/base/common/actions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IProductService } from 'vs/platform/product/common/product'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { Schemas } from 'vs/base/common/network'; +import Severity from 'vs/base/common/severity'; export class OpenUrlAction extends Action { - static readonly ID = 'workbench.action.url.openUrl'; - static readonly LABEL = localize('openUrl', "Open URL"); + static readonly LABEL = localize('openUrl', 'Open URL'); constructor( id: string, label: string, @IURLService private readonly urlService: IURLService, - @IQuickInputService private readonly quickInputService: IQuickInputService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(id, label); } @@ -45,7 +52,7 @@ Registry.as(ActionExtensions.WorkbenchActions).registe const VSCODE_DOMAIN = 'https://code.visualstudio.com'; -const configureTrustedDomainsHandler = ( +const configureTrustedDomainsHandler = async ( quickInputService: IQuickInputService, storageService: IStorageService, domainToConfigure?: string @@ -66,7 +73,7 @@ const configureTrustedDomainsHandler = ( type: 'item', label: d, id: d, - picked: true, + picked: true }; }); @@ -91,23 +98,24 @@ const configureTrustedDomainsHandler = ( specialQuickPickItems.push(domainToConfigureItem); } - const quickPickItems: (IQuickPickItem | IQuickPickSeparator)[] = domainQuickPickItems.length === 0 - ? specialQuickPickItems - : [...specialQuickPickItems, { type: 'separator' }, ...domainQuickPickItems]; + const quickPickItems: (IQuickPickItem | IQuickPickSeparator)[] = + domainQuickPickItems.length === 0 + ? specialQuickPickItems + : [...specialQuickPickItems, { type: 'separator' }, ...domainQuickPickItems]; - return quickInputService.pick(quickPickItems, { + const pickedResult = await quickInputService.pick(quickPickItems, { canPickMany: true, activeItem: domainToConfigureItem - }).then(result => { - if (result) { - const pickedDomains = result.map(r => r.id); - storageService.store('http.trustedDomains', JSON.stringify(pickedDomains), StorageScope.GLOBAL); - - return pickedDomains; - } - - return []; }); + + if (pickedResult) { + const pickedDomains: string[] = pickedResult.map(r => r.id!); + storageService.store('http.trustedDomains', JSON.stringify(pickedDomains), StorageScope.GLOBAL); + + return pickedDomains; + } + + return []; }; const configureTrustedDomainCommand = { @@ -131,3 +139,93 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: configureTrustedDomainCommand.description.description } }); + +class OpenerValidatorContributions implements IWorkbenchContribution { + constructor( + @IOpenerService private readonly _openerService: IOpenerService, + @IStorageService private readonly _storageService: IStorageService, + @IDialogService private readonly _dialogService: IDialogService, + @IProductService private readonly _productService: IProductService, + @IQuickInputService private readonly _quickInputService: IQuickInputService + ) { + this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); + } + + async validateLink(resource: URI): Promise { + const { scheme, authority } = resource; + + if (!equalsIgnoreCase(scheme, Schemas.http) && !equalsIgnoreCase(scheme, Schemas.https)) { + return true; + } + + let trustedDomains: string[] = [VSCODE_DOMAIN]; + try { + const trustedDomainsSrc = this._storageService.get('http.trustedDomains', StorageScope.GLOBAL); + if (trustedDomainsSrc) { + trustedDomains = JSON.parse(trustedDomainsSrc); + } + } catch (err) { } + + const domainToOpen = `${scheme}://${authority}`; + + if (isDomainTrusted(domainToOpen, trustedDomains)) { + return true; + } else { + const choice = await this._dialogService.show( + Severity.Info, + localize( + 'openExternalLinkAt', + 'Do you want {0} to open the external website?\n{1}', + this._productService.nameShort, + resource.toString(true) + ), + [ + localize('openLink', 'Open Link'), + localize('cancel', 'Cancel'), + localize('configureTrustedDomains', 'Configure Trusted Domains') + ], + { + cancelId: 1 + } + ); + + // Open Link + if (choice === 0) { + return true; + } + // Configure Trusted Domains + else if (choice === 2) { + const pickedDomains = await configureTrustedDomainsHandler(this._quickInputService, this._storageService, domainToOpen); + if (pickedDomains.indexOf(domainToOpen) !== -1) { + return true; + } + return false; + } + + return false; + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( + OpenerValidatorContributions, + LifecyclePhase.Restored +); + +/** + * Check whether a domain like https://www.microsoft.com matches + * the list of trusted domains. + */ +function isDomainTrusted(domain: string, trustedDomains: string[]) { + for (let i = 0; i < trustedDomains.length; i++) { + if (trustedDomains[i] === '*') { + return true; + } + + if (trustedDomains[i] === domain) { + return true; + } + } + + return false; +} diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index 73a50a2a9d..6910912e15 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -153,6 +153,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEd reload(): void { this.withWebview(webview => webview.reload()); } showFind(): void { this.withWebview(webview => webview.showFind()); } hideFind(): void { this.withWebview(webview => webview.hideFind()); } + runFindAction(previous: boolean): void { this.withWebview(webview => webview.runFindAction(previous)); } public getInnerWebview() { return this._webview.value; diff --git a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts index 05448c4076..ce06cf2b31 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts @@ -15,8 +15,8 @@ import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } fro import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; -import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, webviewDeveloperCategory } from 'vs/workbench/contrib/webview/browser/webview'; -import { HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand } from '../browser/webviewCommands'; +import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, webviewDeveloperCategory, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; +import { HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands'; import { WebviewEditor } from '../browser/webviewEditor'; import { WebviewEditorInput } from '../browser/webviewEditorInput'; import { IWebviewEditorService, WebviewEditorService } from '../browser/webviewEditorService'; @@ -58,6 +58,28 @@ function registerWebViewCommands(editorId: string): void { weight: KeybindingWeight.EditorContrib } })).register(); + + (new WebViewEditorFindNextCommand({ + id: WebViewEditorFindNextCommand.ID, + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED), + kbOpts: { + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } + })).register(); + + (new WebViewEditorFindPreviousCommand({ + id: WebViewEditorFindPreviousCommand.ID, + precondition: ContextKeyExpr.and( + contextKeyExpr, + KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED), + kbOpts: { + primary: KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + } + })).register(); } registerWebViewCommands(WebviewEditor.ID); diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 2ab61bd62c..a403e797de 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -16,6 +16,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' * Set when the find widget in a webview is visible. */ export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey('webviewFindWidgetVisible', false); +export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED = new RawContextKey('webviewFindWidgetFocused', false); export const IWebviewService = createDecorator('webviewService'); @@ -83,6 +84,7 @@ export interface Webview extends IDisposable { showFind(): void; hideFind(): void; + runFindAction(previous: boolean): void; } export interface WebviewElement extends Webview { diff --git a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts index a8de3a3395..1c7f26efa7 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts @@ -32,6 +32,27 @@ export class HideWebViewEditorFindCommand extends Command { } } +export class WebViewEditorFindNextCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.findNext'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.find(false); + } + } +} + +export class WebViewEditorFindPreviousCommand extends Command { + public static readonly ID = 'editor.action.webvieweditor.findPrevious'; + + public runCommand(accessor: ServicesAccessor, args: any): void { + const webViewEditor = getActiveWebviewEditor(accessor); + if (webViewEditor) { + webViewEditor.find(true); + } + } +} export class ReloadWebviewAction extends Action { static readonly ID = 'workbench.action.webview.reloadWebviewAction'; static readonly LABEL = nls.localize('refreshWebviewLabel', "Reload Webviews"); @@ -62,4 +83,4 @@ function getActiveWebviewEditor(accessor: ServicesAccessor): WebviewEditor | nul const editorService = accessor.get(IEditorService); const activeControl = editorService.activeControl as WebviewEditor; return activeControl.isWebviewEditor ? activeControl : null; -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index 2a3c0e2327..7f5d8a0db5 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -25,7 +25,6 @@ export class WebviewEditor extends BaseEditor { private readonly _scopedContextKeyService = this._register(new MutableDisposable()); private _findWidgetVisible: IContextKey; - private _editorFrame?: HTMLElement; private _content?: HTMLElement; @@ -79,6 +78,12 @@ export class WebviewEditor extends BaseEditor { this.withWebview(webview => webview.hideFind()); } + public find(previous: boolean) { + this.withWebview(webview => { + webview.runFindAction(previous); + }); + } + public reload() { this.withWebview(webview => webview.reload()); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index c6e0cdc7ab..b0961ed4e8 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -268,6 +268,10 @@ export class IFrameWebview extends Disposable implements Webview { throw new Error('Method not implemented.'); } + runFindAction(previous: boolean): void { + throw new Error('Method not implemented.'); + } + public set state(state: string | undefined) { this.content = { html: this.content.html, diff --git a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts index a05e21a970..003192f244 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; export interface WebviewFindDelegate { find(value: string, previous: boolean): void; @@ -15,6 +16,7 @@ export interface WebviewFindDelegate { } export class WebviewFindWidget extends SimpleFindWidget { + protected _findWidgetFocused: IContextKey; constructor( private readonly _delegate: WebviewFindDelegate, @@ -22,6 +24,7 @@ export class WebviewFindWidget extends SimpleFindWidget { @IContextKeyService contextKeyService: IContextKeyService ) { super(contextViewService, contextKeyService); + this._findWidgetFocused = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); } public find(previous: boolean) { @@ -47,9 +50,13 @@ export class WebviewFindWidget extends SimpleFindWidget { return false; } - protected onFocusTrackerFocus() { } + protected onFocusTrackerFocus() { + this._findWidgetFocused.set(true); + } - protected onFocusTrackerBlur() { } + protected onFocusTrackerBlur() { + this._findWidgetFocused.reset(); + } protected onFindInputFocusTrackerFocus() { } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 9b99555bc4..1b18a98f04 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -319,7 +319,7 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { return; case 'did-click-link': - let [uri] = event.args; + const [uri] = event.args; this._onDidClickLink.fire(URI.parse(uri)); return; @@ -334,12 +334,10 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { clientY: rawEvent.clientY + bounds.top, })); return; - } - catch (TypeError) { + } catch { // CustomEvent was treated as MouseEvent so don't do anything - https://github.com/microsoft/vscode/issues/78915 return; } - } case 'did-set-content': @@ -640,6 +638,12 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview { } } + public runFindAction(previous: boolean) { + if (this._webviewFindWidget) { + this._webviewFindWidget.find(previous); + } + } + public reload() { this.doUpdateContent(); } diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.md b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.md index ffd95c8971..1f306f79ef 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.md +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/vs_code_editor_walkthrough.md @@ -5,7 +5,6 @@ The core editor in VS Code is packed with features. This page highlights a numb * [IntelliSense](#intellisense) - get code assistance and parameter suggestions for your code and external modules. * [Line Actions](#line-actions) - quickly move lines around to re-order your code. * [Rename Refactoring](#rename-refactoring) - quickly rename symbols across your code base. -* [Refactoring via Extraction](#refactoring-via-extraction) - quickly extract common code into a separate function or constant. * [Formatting](#formatting) - keep your code looking great with inbuilt document & selection formatting. * [Code Folding](#code-folding) - focus on the most relevant parts of your code by folding other areas. * [Errors and Warnings](#errors-and-warnings) - see errors and warning as you type. @@ -90,21 +89,6 @@ function Book(title, author) { > **JSDoc Tip:** VS Code's IntelliSense uses JSDoc comments to provide richer suggestions. The types and documentation from JSDoc comments show up when you hover over a reference to `Book` or in IntelliSense when you create a new instance of `Book`. -### Refactoring via Extraction -Sometimes you want to refactor already written code into a separate function or constant to reuse it later. Select the lines you want to refactor out and press kb(editor.action.quickFix) or click the little light bulb and choose one of the respective `Extract to...` options. Try it by selecting the code inside the `if`-clause on line 3 or any other common code you want to refactor out. - -```js -function findFirstEvenNumber(arr) { - for (const el of arr) { - if (typeof el === 'number' && el % 2 === 0) { - return el; - } - } - return null; -} -``` - - ### Formatting Keeping your code looking great is hard without a good formatter. Luckily it's easy to format content, either for the entire document with kb(editor.action.formatDocument) or for the current selection with kb(editor.action.formatSelection). Both of these options are also available through the right-click context menu. diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 0cd0d5e7f7..c88880ca4b 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -275,7 +275,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Respect option to reveal an editor if it is already visible in any group if (options && options.revealIfVisible) { for (const group of groupsByLastActive) { - if (input.matches(group.activeEditor)) { + if (group.isActive(input)) { targetGroup = group; break; } @@ -283,11 +283,16 @@ export class EditorService extends Disposable implements EditorServiceImpl { } // Respect option to reveal an editor if it is open (not necessarily visible) - if ((options && options.revealIfOpened) || this.configurationService.getValue('workbench.editor.revealIfOpen')) { - for (const group of groupsByLastActive) { - if (group.isOpened(input)) { - targetGroup = group; - break; + if (!targetGroup) { + if ((options && options.revealIfOpened) || this.configurationService.getValue('workbench.editor.revealIfOpen')) { + for (const group of groupsByLastActive) { + if (group.isOpened(input) && group.isActive(input)) { + targetGroup = group; + break; + } + if (group.isOpened(input) && !targetGroup) { + targetGroup = group; + } } } } diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 8fd60dfc75..0fef8f7e2d 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -62,11 +62,9 @@ export class BrowserWindowConfiguration implements IWindowConfiguration { termProgram?: string; } -export interface IBrowserWindowConfiguration { +interface IBrowserWorkbenchEnvironemntConstructionOptions extends IWorkbenchConstructionOptions { workspaceId: string; - remoteAuthority?: string; - webviewEndpoint?: string; - connectionToken?: string; + logsPath: URI; } export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironmentService { @@ -75,8 +73,10 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment readonly configuration: IWindowConfiguration = new BrowserWindowConfiguration(); - constructor(workspaceId: string, public readonly options: IWorkbenchConstructionOptions) { + constructor(readonly options: IBrowserWorkbenchEnvironemntConstructionOptions) { this.args = { _: [] }; + this.logsPath = options.logsPath.path; + this.logFile = joinPath(options.logsPath, 'window.log'); this.appRoot = '/web/'; this.appNameLong = 'Visual Studio Code - Web'; @@ -88,10 +88,8 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment this.keyboardLayoutResource = joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); this.localeResource = joinPath(this.userRoamingDataHome, 'locale.json'); this.backupHome = joinPath(this.userRoamingDataHome, BACKUPS); - this.configuration.backupWorkspaceResource = joinPath(this.backupHome, workspaceId); - this.configuration.connectionToken = options.connectionToken || this.getConnectionTokenFromLocation(); - - this.logsPath = '/web/logs'; + this.configuration.backupWorkspaceResource = joinPath(this.backupHome, options.workspaceId); + this.configuration.connectionToken = options.connectionToken || getCookieValue('vscode-tkn'); this.debugExtensionHost = { port: null, @@ -183,6 +181,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment driverVerbose: boolean; webviewEndpoint?: string; galleryMachineIdResource?: URI; + readonly logFile: URI; get webviewResourceRoot(): string { return this.webviewEndpoint ? this.webviewEndpoint + '/vscode-resource{{resource}}' : 'vscode-resource:{{resource}}'; @@ -191,21 +190,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get webviewCspSource(): string { return this.webviewEndpoint ? this.webviewEndpoint : 'vscode-resource:'; } - - private getConnectionTokenFromLocation(): string | undefined { - // TODO: Check with @alexd where the token will be: search or hash? - let connectionToken: string | undefined = undefined; - if (document.location.search) { - connectionToken = this.getConnectionToken(document.location.search); - } - if (!connectionToken && document.location.hash) { - connectionToken = this.getConnectionToken(document.location.hash); - } - return connectionToken; - } - - private getConnectionToken(str: string): string | undefined { - const m = str.match(/[#&?]tkn=([^&]+)/); - return m ? m[1] : undefined; - } +} + +/** + * See https://stackoverflow.com/a/25490531 + */ +function getCookieValue(name: string): string | undefined { + const m = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); + return m ? m.pop() : undefined; } diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index 82f8ba4fb3..35224e309f 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -7,6 +7,7 @@ import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/co import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; +import { URI } from 'vs/base/common/uri'; export const IWorkbenchEnvironmentService = createDecorator('environmentService'); @@ -16,5 +17,7 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { readonly configuration: IWindowConfiguration; + readonly logFile: URI; + readonly options?: IWorkbenchConstructionOptions; } diff --git a/src/vs/workbench/services/environment/node/environmentService.ts b/src/vs/workbench/services/environment/node/environmentService.ts index 26cf9a3f0d..c9ce7cdc04 100644 --- a/src/vs/workbench/services/environment/node/environmentService.ts +++ b/src/vs/workbench/services/environment/node/environmentService.ts @@ -11,6 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { toBackupWorkspaceResource } from 'vs/workbench/services/backup/common/backup'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { join } from 'vs/base/common/path'; export class WorkbenchEnvironmentService extends EnvironmentService implements IWorkbenchEnvironmentService { @@ -31,4 +32,7 @@ export class WorkbenchEnvironmentService extends EnvironmentService implements I @memoize get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); } + + @memoize + get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.configuration.windowId}.log`)); } } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 31841423f4..dcb29119b8 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -86,7 +86,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const result: ExtensionHostProcessManager[] = []; const webExtensions = 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 webHostProcessWorker = this._instantiationService.createInstance(WebWorkerExtensionHostStarter, true, webExtensions, URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme })); const webHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, webHostProcessWorker, null, initialActivationEvents); result.push(webHostProcessManager); diff --git a/src/vs/workbench/services/keybinding/browser/keymapService.ts b/src/vs/workbench/services/keybinding/browser/keymapService.ts index 982f41413c..cf8ac659c2 100644 --- a/src/vs/workbench/services/keybinding/browser/keymapService.ts +++ b/src/vs/workbench/services/keybinding/browser/keymapService.ts @@ -176,6 +176,7 @@ export class BrowserKeyboardMapperFactoryBase { } setActiveKeyMapping(keymap: IKeyboardMapping | null) { + let keymapUpdated = false; let matchedKeyboardLayout = this.getMatchedKeymapInfo(keymap); if (matchedKeyboardLayout) { // let score = matchedKeyboardLayout.score; @@ -209,18 +210,21 @@ export class BrowserKeyboardMapperFactoryBase { if (!this._activeKeymapInfo) { this._activeKeymapInfo = matchedKeyboardLayout.result; + keymapUpdated = true; } else if (keymap) { if (matchedKeyboardLayout.result.getScore(keymap) > this._activeKeymapInfo.getScore(keymap)) { this._activeKeymapInfo = matchedKeyboardLayout.result; + keymapUpdated = true; } } } if (!this._activeKeymapInfo) { this._activeKeymapInfo = this.getUSStandardLayout(); + keymapUpdated = true; } - if (!this._activeKeymapInfo) { + if (!this._activeKeymapInfo || !keymapUpdated) { return; } diff --git a/src/vs/workbench/services/keybinding/test/browserKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/browserKeyboardMapper.test.ts index 064ad120cb..8b52815811 100644 --- a/src/vs/workbench/services/keybinding/test/browserKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/browserKeyboardMapper.test.ts @@ -40,12 +40,12 @@ suite('keyboard layout loader', () => { let commandService = instantiationService.stub(ICommandService, {}); let instance = new TestKeyboardMapperFactory(notitifcationService, storageService, commandService); - test.skip('load default US keyboard layout', () => { + test('load default US keyboard layout', () => { assert.notEqual(instance.activeKeyboardLayout, null); - assert.equal(instance.activeKeyboardLayout!.isUSStandard, true); }); - test.skip('isKeyMappingActive', () => { + test('isKeyMappingActive', () => { + instance.setUSKeyboardLayout(); assert.equal(instance.isKeyMappingActive({ KeyA: { value: 'a', diff --git a/src/vs/workbench/services/log/browser/indexedDBLogProvider.ts b/src/vs/workbench/services/log/browser/indexedDBLogProvider.ts new file mode 100644 index 0000000000..323f900c0a --- /dev/null +++ b/src/vs/workbench/services/log/browser/indexedDBLogProvider.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyValueLogProvider } from 'vs/workbench/services/log/common/keyValueLogProvider'; + +export const INDEXEDDB_LOG_SCHEME = 'vscode-logs-indexedbd'; +export const INDEXEDDB_LOGS_DB = 'vscode-logs-db'; +export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store'; + +export class IndexedDBLogProvider extends KeyValueLogProvider { + + private readonly database: Promise; + + constructor( + ) { + super(INDEXEDDB_LOG_SCHEME); + this.database = this.openDatabase(1); + } + + private openDatabase(version: number): Promise { + return new Promise((c, e) => { + const request = window.indexedDB.open(INDEXEDDB_LOGS_DB, version); + request.onerror = (err) => e(request.error); + request.onsuccess = () => { + const db = request.result; + if (db.objectStoreNames.contains(INDEXEDDB_LOGS_OBJECT_STORE)) { + c(db); + } + }; + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(INDEXEDDB_LOGS_OBJECT_STORE)) { + db.createObjectStore(INDEXEDDB_LOGS_OBJECT_STORE); + } + c(db); + }; + }); + } + + protected async getAllKeys(): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.getAllKeys(); + request.onerror = () => e(request.error); + request.onsuccess = () => c(request.result); + }); + } + + protected hasKey(key: string): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.getKey(key); + request.onerror = () => e(request.error); + request.onsuccess = () => { + c(!!request.result); + }; + }); + } + + protected getValue(key: string): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.get(key); + request.onerror = () => e(request.error); + request.onsuccess = () => c(request.result || ''); + }); + } + + protected setValue(key: string, value: string): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE], 'readwrite'); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.put(value, key); + request.onerror = () => e(request.error); + request.onsuccess = () => c(); + }); + } + + protected deleteKey(key: string): Promise { + return new Promise(async (c, e) => { + const db = await this.database; + const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE], 'readwrite'); + const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE); + const request = objectStore.delete(key); + request.onerror = () => e(request.error); + request.onsuccess = () => c(); + }); + } +} diff --git a/src/vs/workbench/services/log/common/inMemoryLogProvider.ts b/src/vs/workbench/services/log/common/inMemoryLogProvider.ts new file mode 100644 index 0000000000..7138d7ec11 --- /dev/null +++ b/src/vs/workbench/services/log/common/inMemoryLogProvider.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyValueLogProvider } from 'vs/workbench/services/log/common/keyValueLogProvider'; +import { keys } from 'vs/base/common/map'; + +export const INMEMORY_LOG_SCHEME = 'vscode-logs-inmemory'; + +export class InMemoryLogProvider extends KeyValueLogProvider { + + private readonly logs: Map = new Map(); + + constructor( + ) { + super(INMEMORY_LOG_SCHEME); + } + + protected async getAllKeys(): Promise { + return keys(this.logs); + } + + protected async hasKey(key: string): Promise { + return this.logs.has(key); + } + + protected async getValue(key: string): Promise { + return this.logs.get(key) || ''; + } + + protected async setValue(key: string, value: string): Promise { + this.logs.set(key, value); + } + + protected async deleteKey(key: string): Promise { + this.logs.delete(key); + } + +} diff --git a/src/vs/workbench/services/log/common/keyValueLogProvider.ts b/src/vs/workbench/services/log/common/keyValueLogProvider.ts new file mode 100644 index 0000000000..a39d2a7119 --- /dev/null +++ b/src/vs/workbench/services/log/common/keyValueLogProvider.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { FileSystemError } from 'vs/workbench/api/common/extHostTypes'; +import { isEqualOrParent, joinPath, relativePath } from 'vs/base/common/resources'; +import { values } from 'vs/base/common/map'; + +export abstract class KeyValueLogProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { + + readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite; + readonly onDidChangeCapabilities: Event = Event.None; + + private readonly _onDidChangeFile: Emitter = this._register(new Emitter()); + readonly onDidChangeFile: Event = this._onDidChangeFile.event; + + private readonly versions: Map = new Map(); + + constructor(private readonly scheme: string) { + super(); + } + + watch(resource: URI, opts: IWatchOptions): IDisposable { + return Disposable.None; + } + + async mkdir(resource: URI): Promise { + } + + async stat(resource: URI): Promise { + try { + const content = await this.readFile(resource); + return { + type: FileType.File, + ctime: 0, + mtime: this.versions.get(resource.toString()) || 0, + size: content.byteLength + }; + } catch (e) { + } + const files = await this.readdir(resource); + if (files.length) { + return { + type: FileType.Directory, + ctime: 0, + mtime: 0, + size: 0 + }; + } + return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotFound)); + } + + async readdir(resource: URI): Promise<[string, FileType][]> { + const hasKey = await this.hasKey(resource.path); + if (hasKey) { + return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotADirectory)); + } + const keys = await this.getAllKeys(); + const files: Map = new Map(); + for (const key of keys) { + const keyResource = this.toResource(key); + if (isEqualOrParent(keyResource, resource, false)) { + const path = relativePath(resource, keyResource, false); + if (path) { + const keySegments = path.split('/'); + files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]); + } + } + } + return values(files); + } + + async readFile(resource: URI): Promise { + const hasKey = await this.hasKey(resource.path); + if (!hasKey) { + return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotFound)); + } + const value = await this.getValue(resource.path); + return VSBuffer.fromString(value).buffer; + } + + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + const hasKey = await this.hasKey(resource.path); + if (!hasKey) { + const files = await this.readdir(resource); + if (files.length) { + return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileIsADirectory)); + } + } + await this.setValue(resource.path, VSBuffer.wrap(content).toString()); + this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1); + this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]); + } + + async delete(resource: URI, opts: FileDeleteOptions): Promise { + const hasKey = await this.hasKey(resource.path); + if (hasKey) { + await this.deleteKey(resource.path); + this.versions.delete(resource.path); + this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]); + return; + } + + if (opts.recursive) { + const files = await this.readdir(resource); + await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts))); + } + } + + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + return Promise.reject(new Error('Not Supported')); + } + + private toResource(key: string): URI { + return URI.file(key).with({ scheme: this.scheme }); + } + + protected abstract getAllKeys(): Promise; + protected abstract hasKey(key: string): Promise; + protected abstract getValue(key: string): Promise; + protected abstract setValue(key: string, value: string): Promise; + protected abstract deleteKey(key: string): Promise; +} diff --git a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts index 871436e0d8..38be7fe6e2 100644 --- a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts +++ b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts @@ -47,7 +47,7 @@ suite('FileUserDataProvider', () => { userDataResource = URI.file(userDataPath).with({ scheme: Schemas.userData }); await Promise.all([pfs.mkdirp(userDataPath), pfs.mkdirp(backupsPath)]); - const environmentService = new BrowserWorkbenchEnvironmentService('workspaceId', { remoteAuthority: 'remote' }); + const environmentService = new BrowserWorkbenchEnvironmentService({ remoteAuthority: 'remote', workspaceId: 'workspaceId', logsPath: URI.file('logFile') }); environmentService.userRoamingDataHome = userDataResource; const userDataFileSystemProvider = new FileUserDataProvider(URI.file(userDataPath), URI.file(backupsPath), diskFileSystemProvider, environmentService); @@ -321,7 +321,7 @@ suite('FileUserDataProvider - Watching', () => { localUserDataResource = URI.file(userDataPath); userDataResource = localUserDataResource.with({ scheme: Schemas.userData }); - const environmentService = new BrowserWorkbenchEnvironmentService('workspaceId', { remoteAuthority: 'remote' }); + const environmentService = new BrowserWorkbenchEnvironmentService({ remoteAuthority: 'remote', workspaceId: 'workspaceId', logsPath: URI.file('logFile') }); environmentService.userRoamingDataHome = userDataResource; const userDataFileSystemProvider = new FileUserDataProvider(localUserDataResource, localBackupsResource, new TestFileSystemProvider(fileEventEmitter.event), environmentService); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index f96f3b0b76..72bab529b5 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -201,9 +201,6 @@ registerSingleton(IAdsTelemetryService, AdsTelemetryService); // Localizations import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; -// Logs -import 'vs/workbench/contrib/logs/electron-browser/logs.contribution'; - // Stats import 'vs/workbench/contrib/stats/electron-browser/workspaceStatsService'; import 'vs/workbench/contrib/stats/electron-browser/stats.contribution'; diff --git a/test/electron/renderer.html b/test/electron/renderer.html index 7245438959..a2d4bcc5ad 100644 --- a/test/electron/renderer.html +++ b/test/electron/renderer.html @@ -11,6 +11,18 @@