From 249bf465aef81462a57152c0b32ef5740de54c9e Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Wed, 5 May 2021 23:21:36 -0500 Subject: [PATCH 001/270] add color customizations for inline debug values --- .../debug/browser/debugEditorContribution.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index c4a86e5c53c..0e8ca058724 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -45,6 +45,8 @@ import { Event } from 'vs/base/common/event'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Expression } from 'vs/workbench/contrib/debug/common/debugModel'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; const LAUNCH_JSON_REGEX = /\.vscode\/launch\.json$/; const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration'; @@ -52,6 +54,20 @@ const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped +export const debugInlineForeground = registerColor('debugInline.decorationForeground', { + dark: '#ffffff80', + light: '#00000080', + hc: '#ffffff80' +}, nls.localize('debug.inline.decorationForeground', "Color for the inline debug text.")); + +export const debugInlineBackground = registerColor('debugInline.decorationBackground', { + dark: '#ffc80033', + light: '#ffc80033', + hc: '#ffc80033' +}, nls.localize('debug.inline.decorationBackground', "Color for the inline debug background.")); + + + class InlineSegment { constructor(public column: number, public text: string) { } @@ -73,18 +89,9 @@ function createInlineValueDecoration(lineNumber: number, contentText: string, co renderOptions: { after: { contentText, - backgroundColor: 'rgba(255, 200, 0, 0.2)', - margin: '10px' - }, - dark: { - after: { - color: 'rgba(255, 255, 255, 0.5)', - } - }, - light: { - after: { - color: 'rgba(0, 0, 0, 0.5)', - } + backgroundColor: themeColorFromId(debugInlineBackground), + margin: '10px', + color: themeColorFromId(debugInlineForeground) } } }; From 0c83aaf20a3523b84a56c1d4096205b38397c25b Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Wed, 5 May 2021 23:33:40 -0500 Subject: [PATCH 002/270] fix localization key/description for debugInline colors --- .../contrib/debug/browser/debugEditorContribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 0e8ca058724..3fc861c3648 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -58,13 +58,13 @@ export const debugInlineForeground = registerColor('debugInline.decorationForegr dark: '#ffffff80', light: '#00000080', hc: '#ffffff80' -}, nls.localize('debug.inline.decorationForeground', "Color for the inline debug text.")); +}, nls.localize('debugInline.decorationForeground', "Color for the debug inline value text.")); export const debugInlineBackground = registerColor('debugInline.decorationBackground', { dark: '#ffc80033', light: '#ffc80033', hc: '#ffc80033' -}, nls.localize('debug.inline.decorationBackground', "Color for the inline debug background.")); +}, nls.localize('debugInline.decorationBackground', "Color for the debug inline value background.")); From e3b7e6a6c19d6f72b3111c0a7d7ccffe19212577 Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Thu, 6 May 2021 22:35:59 -0500 Subject: [PATCH 003/270] remove excess whitespace --- .../workbench/contrib/debug/browser/debugEditorContribution.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 3fc861c3648..84112893d02 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -66,8 +66,6 @@ export const debugInlineBackground = registerColor('debugInline.decorationBackgr hc: '#ffc80033' }, nls.localize('debugInline.decorationBackground', "Color for the debug inline value background.")); - - class InlineSegment { constructor(public column: number, public text: string) { } From f75ade6e43122bee9c8444f306a0773ceb6bc47a Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Fri, 7 May 2021 23:58:50 -0500 Subject: [PATCH 004/270] Add showDeprecated option to suggest, filters out deprecated options --- src/vs/editor/common/config/commonEditorConfig.ts | 1 + src/vs/editor/common/config/editorOptions.ts | 11 +++++++++++ src/vs/editor/contrib/suggest/completionModel.ts | 5 ++++- .../contrib/suggest/test/completionModel.test.ts | 1 + src/vs/monaco.d.ts | 4 ++++ 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 4a888eb21e7..3ec9a2093e7 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -204,6 +204,7 @@ function migrateOptions(options: IEditorOptions): void { mapping['method'] = 'showMethods'; mapping['function'] = 'showFunctions'; mapping['constructor'] = 'showConstructors'; + mapping['deprecated'] = 'showDeprecated'; mapping['field'] = 'showFields'; mapping['variable'] = 'showVariables'; mapping['class'] = 'showClasses'; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 4eb14e26c69..a1ebca1d549 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -3186,6 +3186,10 @@ export interface ISuggestOptions { * Show constructor-suggestions. */ showConstructors?: boolean; + /** + * Show deprecated-suggestions. + */ + showDeprecated?: boolean; /** * Show field-suggestions. */ @@ -3301,6 +3305,7 @@ class EditorSuggest extends BaseEditorOption Date: Wed, 12 May 2021 16:59:57 -0600 Subject: [PATCH 005/270] scroll state persists while toggling between static preview and text --- .../src/features/preview.ts | 31 +++++++++------ .../src/features/previewManager.ts | 10 ++++- .../src/util/topmostLineMonitor.ts | 38 +++++++++++++++++++ 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 42d6f08fc07..a3b32554d59 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -12,7 +12,7 @@ import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; import { normalizeResource, WebviewResourceProvider } from '../util/resources'; -import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor'; +import { getVisibleLine, scrollEditorToLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider'; import { MarkdownEngine } from '../markdownEngine'; @@ -120,6 +120,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = []; private readonly _fileWatchersBySrc = new Map(); + private readonly _onScrollEmitter = this._register(new vscode.EventEmitter()); + public readonly onScroll = this._onScrollEmitter.event; constructor( webview: vscode.WebviewPanel, @@ -324,7 +326,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private onDidScrollPreview(line: number) { this.line = line; - + this._onScrollEmitter.fire({ line: this.line, uri: this._resource }); const config = this._previewConfigurations.loadAndCacheConfiguration(this._resource); if (!config.scrollEditorWithPreview) { return; @@ -336,13 +338,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } this.isScrolling = true; - const sourceLine = Math.floor(line); - const fraction = line - sourceLine; - const text = editor.document.lineAt(sourceLine).text; - const start = Math.floor(fraction * text.length); - editor.revealRange( - new vscode.Range(sourceLine, start, sourceLine + 1, 0), - vscode.TextEditorRevealType.AtTop); + scrollEditorToLine(line, editor); } } @@ -500,8 +496,9 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown logger: Logger, contributionProvider: MarkdownContributionProvider, engine: MarkdownEngine, + scrollLine?: number, ): StaticMarkdownPreview { - return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider, engine); + return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider, engine, scrollLine); } private readonly preview: MarkdownPreview; @@ -514,10 +511,11 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown logger: Logger, contributionProvider: MarkdownContributionProvider, engine: MarkdownEngine, + scrollLine?: number, ) { super(); - - this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, undefined, { + const topScrollLocation = scrollLine ? new StartingScrollLine(scrollLine) : undefined; + this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, { getAdditionalState: () => { return {}; }, openPreviewLinkToMarkdownFile: () => { /* todo */ } }, engine, contentProvider, _previewConfigurations, logger, contributionProvider)); @@ -529,6 +527,12 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown this._register(this._webviewPanel.onDidChangeViewState(e => { this._onDidChangeViewState.fire(e); })); + this._register(this.preview.onScroll((scrollInfo) => { + this._onScrollEmitter.fire(scrollInfo); + })); + + const currentLine = this.preview.state.line ? this.preview.state.line : 0; + this._onScrollEmitter.fire({ line: currentLine, uri: this.preview.resource }); } private readonly _onDispose = this._register(new vscode.EventEmitter()); @@ -537,6 +541,9 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown private readonly _onDidChangeViewState = this._register(new vscode.EventEmitter()); public readonly onDidChangeViewState = this._onDidChangeViewState.event; + private readonly _onScrollEmitter = this._register(new vscode.EventEmitter()); + public readonly onScroll = this._onScrollEmitter.event; + override dispose() { this._onDispose.fire(); super.dispose(); diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index b93a5061053..85fc99d67c9 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -160,6 +160,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview document: vscode.TextDocument, webview: vscode.WebviewPanel ): Promise { + const lineNumber = this._topmostLineMonitor.previousMDTextEditor?.document.uri.toString() === document.uri.toString() ? this._topmostLineMonitor.previousMDTextEditor?.visibleRanges[0].start.line : undefined; const preview = StaticMarkdownPreview.revive( document.uri, webview, @@ -167,7 +168,9 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview this._previewConfigurations, this._logger, this._contributions, - this._engine); + this._engine, + lineNumber + ); this.registerStaticPreview(preview); } @@ -220,6 +223,11 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview this._staticPreviews.delete(preview); }); + // Continuously update the scroll info in case user changes the editor. + preview.onScroll((scrollInfo) => { + this._topmostLineMonitor.previousStaticEditorInfo = scrollInfo; + }); + this.trackActive(preview); return preview; } diff --git a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index 57395fbc691..0b66b92cfc1 100644 --- a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -7,13 +7,21 @@ import * as vscode from 'vscode'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from './file'; +export interface LastScrollLocation { + readonly line: number; + readonly uri: vscode.Uri | undefined; +} + export class TopmostLineMonitor extends Disposable { private readonly pendingUpdates = new Map(); private readonly throttle = 50; + public previousMDTextEditor: vscode.TextEditor | undefined; + public previousStaticEditorInfo: LastScrollLocation = { line: 0, uri: undefined }; constructor() { super(); + this.previousMDTextEditor = vscode.window.activeTextEditor; this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => { if (isMarkdownFile(event.textEditor.document)) { const line = getVisibleLine(event.textEditor); @@ -22,6 +30,21 @@ export class TopmostLineMonitor extends Disposable { } } })); + + this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => { + + // When at a markdown file, apply existing scroll settings from static preview if applicable. + // Also save reference to text editor for line number reference later + if (textEditor && isMarkdownFile(textEditor.document!)) { + + if (this.previousStaticEditorInfo.uri?.toString() === textEditor.document.uri.toString()) { + const line = this.previousStaticEditorInfo.line ? this.previousStaticEditorInfo.line : 0; + scrollEditorToLine(line, textEditor); + } + + this.previousMDTextEditor = textEditor; + } + })); } private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); @@ -68,3 +91,18 @@ export function getVisibleLine( const progress = firstVisiblePosition.character / (line.text.length + 2); return lineNumber + progress; } +/** + * Change the top-most visible line of `editor` to be at `line` + */ +export function scrollEditorToLine( + line: number, + editor: vscode.TextEditor +) { + const sourceLine = Math.floor(line); + const fraction = line - sourceLine; + const text = editor.document.lineAt(sourceLine).text; + const start = Math.floor(fraction * text.length); + editor.revealRange( + new vscode.Range(sourceLine, start, sourceLine + 1, 0), + vscode.TextEditorRevealType.AtTop); +} From d2894562d574da1c33621365c5c57d81cb9936b4 Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Wed, 12 May 2021 17:59:00 -0600 Subject: [PATCH 006/270] fixing issue with static scroll alongside text editor --- .../markdown-language-features/src/features/preview.ts | 10 +++++++++- .../src/features/previewManager.ts | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index a3b32554d59..71162b60e8f 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -493,12 +493,13 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown webview: vscode.WebviewPanel, contentProvider: MarkdownContentProvider, previewConfigurations: MarkdownPreviewConfigurationManager, + topmostLineMonitor: TopmostLineMonitor, logger: Logger, contributionProvider: MarkdownContributionProvider, engine: MarkdownEngine, scrollLine?: number, ): StaticMarkdownPreview { - return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider, engine, scrollLine); + return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, engine, scrollLine); } private readonly preview: MarkdownPreview; @@ -508,6 +509,7 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown resource: vscode.Uri, contentProvider: MarkdownContentProvider, private readonly _previewConfigurations: MarkdownPreviewConfigurationManager, + topmostLineMonitor: TopmostLineMonitor, logger: Logger, contributionProvider: MarkdownContributionProvider, engine: MarkdownEngine, @@ -531,6 +533,12 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown this._onScrollEmitter.fire(scrollInfo); })); + this._register(topmostLineMonitor.onDidChanged(event => { + if (this.preview.isPreviewOf(event.resource)) { + this.preview.scrollTo(event.line); + } + })); + const currentLine = this.preview.state.line ? this.preview.state.line : 0; this._onScrollEmitter.fire({ line: currentLine, uri: this.preview.resource }); } diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 85fc99d67c9..7b94522808e 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -166,6 +166,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview webview, this._contentProvider, this._previewConfigurations, + this._topmostLineMonitor, this._logger, this._contributions, this._engine, From 66fa5c41b3afd35f1cc28dbf9aab017c422b5b0f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 12 May 2021 19:32:57 -0700 Subject: [PATCH 007/270] First cut at unifying notebook renderers apis This implements the api described in #123540. Major points: - Instead of having the `markdown-it` renderer pull it its dependencies, instead the dependencies can call `getRenderer` to import the object returned by the `markdown-it` renderer - We try to detect if a renderer is using the old or new api. Old renderers are still run as globals while new ones are loaded with `import` - I have only hooked up the new API for markdown renderers so far --- .../notebook/index.ts | 24 +- .../notebook/emoji.ts | 10 +- .../notebook/katex.ts | 37 +-- .../view/renderers/backLayerWebView.ts | 37 +-- .../browser/view/renderers/webviewPreloads.ts | 231 ++++++++++++------ 5 files changed, 207 insertions(+), 132 deletions(-) diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index b7c2df85817..7b44fba6583 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -5,35 +5,23 @@ const MarkdownIt = require('markdown-it'); -export async function activate(ctx: { - dependencies: ReadonlyArray<{ entrypoint: string }> -}) { +export function activate() { let markdownIt = new MarkdownIt({ html: true }); - // Should we load the deps before this point? - // Also could we await inside `renderMarkup`? - await Promise.all(ctx.dependencies.map(async (dep) => { - try { - const api = await import(dep.entrypoint); - if (api?.extendMarkdownIt) { - markdownIt = api.extendMarkdownIt(markdownIt); - } - } catch (e) { - console.error('Could not load markdown entryPoint', e); - } - })); - return { - renderMarkup: (context: { element: HTMLElement, content: string }) => { - const rendered = markdownIt.render(context.content); + renderCell: (_id: string, context: { element: HTMLElement, value: string }) => { + const rendered = markdownIt.render(context.value); context.element.innerHTML = rendered; // Insert styles into markdown preview shadow dom so that they are applied for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) { context.element.insertAdjacentElement('beforebegin', markdownStyleNode.cloneNode(true) as Element); } + }, + extendMarkdownIt: (f: (md: typeof markdownIt) => void) => { + f(markdownIt); } }; } diff --git a/extensions/notebook-markdown-extensions/notebook/emoji.ts b/extensions/notebook-markdown-extensions/notebook/emoji.ts index bf82f98ba0f..b842750a03c 100644 --- a/extensions/notebook-markdown-extensions/notebook/emoji.ts +++ b/extensions/notebook-markdown-extensions/notebook/emoji.ts @@ -6,6 +6,12 @@ import type * as markdownIt from 'markdown-it'; const emoji = require('markdown-it-emoji'); -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(emoji); +export function activate(ctx: { + getRenderer: (id: string) => any +}) { + const markdownItRenderer = ctx.getRenderer('markdownItRenderer'); + + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(emoji); + }); } diff --git a/extensions/notebook-markdown-extensions/notebook/katex.ts b/extensions/notebook-markdown-extensions/notebook/katex.ts index 910036babf2..ccb12569053 100644 --- a/extensions/notebook-markdown-extensions/notebook/katex.ts +++ b/extensions/notebook-markdown-extensions/notebook/katex.ts @@ -6,23 +6,28 @@ import type * as markdownIt from 'markdown-it'; const styleHref = import.meta.url.replace(/katex.js$/, 'katex.min.css'); -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.classList.add('markdown-style'); -link.href = styleHref; -document.head.append(link); +export function activate(ctx: { + getRenderer: (id: string) => any +}) { + const markdownItRenderer = ctx.getRenderer('markdownItRenderer'); -const style = document.createElement('style'); -style.classList.add('markdown-style'); -style.textContent = ` - .katex-error { - color: var(--vscode-editorError-foreground); - } -`; -document.head.append(style); + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.classList.add('markdown-style'); + link.href = styleHref; + document.head.append(link); -const katex = require('@iktakahiro/markdown-it-katex'); + const style = document.createElement('style'); + style.classList.add('markdown-style'); + style.textContent = ` + .katex-error { + color: var(--vscode-editorError-foreground); + } + `; + document.head.append(style); -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(katex); + const katex = require('@iktakahiro/markdown-it-katex'); + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(katex); + }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 7fc1c3f8bf3..bfa244d863d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -26,10 +26,10 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; import { CellEditState, ICellOutputViewModel, ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { preloadsScriptStr, WebviewPreloadRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; +import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; -import { INotebookKernel, INotebookRendererInfo, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewContentPurpose, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -453,7 +453,7 @@ export class BackLayerWebView extends Disposable { this.element.style.position = 'absolute'; } private generateContent(coreDependencies: string, baseUrl: string) { - const markupRenderer = this.getMarkdownRenderer(); + const renderersData = this.getRendererData(); const outputWidth = `calc(100% - ${this.options.leftMargin + this.options.rightMargin + this.options.runGutter}px)`; const outputMarginLeft = `${this.options.leftMargin + this.options.runGutter}px`; return html` @@ -707,36 +707,19 @@ export class BackLayerWebView extends Disposable { ${coreDependencies}
- + `; } - private getMarkdownRenderer(): WebviewPreloadRenderer[] { - const markdownMimeType = 'text/markdown'; - const allRenderers = this.notebookService.getRenderers() - .filter(renderer => renderer.matchesWithoutKernel(markdownMimeType) !== NotebookRendererMatch.Never); - - const topLevelMarkdownRenderers = allRenderers - .filter(renderer => renderer.dependencies.length === 0); - - const subRenderers = new Map>(); - for (const renderer of allRenderers) { - for (const dep of renderer.dependencies) { - if (!subRenderers.has(dep)) { - subRenderers.set(dep, []); - } - const entryPoint = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation); - subRenderers.get(dep)!.push({ entrypoint: entryPoint.toString(true) }); - } - } - - return topLevelMarkdownRenderers.map((renderer): WebviewPreloadRenderer => { - const src = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation); + private getRendererData(): RendererMetadata[] { + return this.notebookService.getRenderers().map((renderer): RendererMetadata => { + const entrypoint = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation).toString(); return { - entrypoint: src.toString(), + id: renderer.id, + entrypoint, mimeTypes: renderer.mimeTypes, - dependencies: subRenderers.get(renderer.id) || [], + dependencies: Array.from(renderer.dependencies.values()) }; }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 54e0ed72b83..9f0e468f5c6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -41,7 +41,7 @@ interface PreloadStyles { declare function __import(path: string): Promise; -async function webviewPreloads(style: PreloadStyles, rendererData: readonly WebviewPreloadRenderer[]) { +async function webviewPreloads(style: PreloadStyles, rendererData: readonly RendererMetadata[]) { const acquireVsCodeApi = globalThis.acquireVsCodeApi; const vscode = acquireVsCodeApi(); delete (globalThis as any).acquireVsCodeApi; @@ -111,32 +111,68 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } }; - const runScript = async (url: string, originalUri: string, globals: { [name: string]: unknown } = {}): Promise<() => (PreloadResult)> => { - let text: string; - try { - const res = await fetch(url); - text = await res.text(); - if (!res.ok) { - throw new Error(`Unexpected ${res.status} requesting ${originalUri}: ${text || res.statusText}`); - } - - globals.scriptUrl = url; - } catch (e) { - return () => ({ state: PreloadState.Error, error: e.message }); + async function loadScriptSource(url: string, originalUri = url): Promise { + const res = await fetch(url); + const text = await res.text(); + if (!res.ok) { + throw new Error(`Unexpected ${res.status} requesting ${originalUri}: ${text || res.statusText}`); } + return text; + } + + interface RendererContext { + getState(): T | undefined; + setState(newState: T): void; + + getRenderer(id: string): any | undefined; + } + + function createRendererContext(rendererId: string): RendererContext { + const api = acquireNotebookRendererApi(rendererId); + return { + getState: api.getState.bind(api), + setState: api.setState.bind(api), + getRenderer: (id: string) => renderers.getRenderer(id), + }; + } + + interface ScriptModule { + activate: (ctx?: RendererContext) => any; + } + + const invokeSourceWithGlobals = (functionSrc: string, globals: { [name: string]: unknown }) => { const args = Object.entries(globals); - return () => { - try { - new Function(...args.map(([k]) => k), text)(...args.map(([, v]) => v)); - return { state: PreloadState.Ok }; - } catch (e) { - console.error(e); - return { state: PreloadState.Error, error: e.message }; + return new Function(...args.map(([k]) => k), functionSrc)(...args.map(([, v]) => v)); + }; + + const runPreload = async (url: string, originalUri: string): Promise => { + const text = await loadScriptSource(url, originalUri); + return { + activate: () => { + return invokeSourceWithGlobals(text, kernelPreloadGlobals); } }; }; + const runRenderScript = async (url: string, rendererId: string): Promise => { + const text = await loadScriptSource(url); + // TODO: Support both the new module based renderers and the old style global renderers + const isModule = /\bexport\b.*\bactivate\b/.test(text); + if (isModule) { + return __import(url); + } else { + return { + activate: () => { + const globals = { + acquireNotebookRendererApi: () => acquireNotebookRendererApi(rendererId) + }; + return invokeSourceWithGlobals(text, globals); + } + }; + } + }; + const dimensionUpdater = new class { private readonly pending = new Map(); @@ -389,7 +425,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv // the dontEmit symbol to skip emission. function mapEmitter(emitter: EmitterLike, mapFn: (data: T) => R | typeof dontEmit) { let listener: IDisposable; - const mapped = createEmitter(listeners => { + const mapped = createEmitter(listeners => { if (listeners.size && !listener) { listener = emitter.event(data => { const v = mapFn(data); @@ -407,7 +443,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv interface ICreateCellInfo { element: HTMLElement; - outputId: string; + outputId?: string; mime: string; value: unknown; @@ -422,7 +458,14 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv const onDidCreateOutput = createEmitter<{ rendererId: string, info: ICreateCellInfo }>(); const onDidReceiveKernelMessage = createEmitter(); - const acquireNotebookRendererApi = (id: string) => ({ + interface GlobalNotebookRendererApi { + setState: (newState: T) => void; + getState(): T | undefined; + readonly onWillDestroyOutput: Event; + readonly onDidCreateOutput: Event; + } + + const acquireNotebookRendererApi = (id: string): GlobalNotebookRendererApi => ({ setState(newState: T) { vscode.setState({ ...vscode.getState(), [id]: newState }); }, @@ -632,6 +675,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv cellOutputContainer.appendChild(outputContainer); outputContainer.appendChild(outputNode); } else { + // TODO: this should go through renderers instead onDidCreateOutput.fire({ rendererId: data.rendererId!, info: { @@ -754,21 +798,32 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv const resources = event.data.resources; let queue: Promise = Promise.resolve({ state: PreloadState.Ok }); for (const { uri, originalUri, source } of resources) { - const globals = source === 'kernel' - ? kernelPreloadGlobals - : { acquireNotebookRendererApi: () => acquireNotebookRendererApi(source.rendererId) }; - // create the promise so that the scripts download in parallel, but // only invoke them in series within the queue - const promise = runScript(uri, originalUri, globals); - queue = queue.then(() => promise.then(fn => { - const result = fn(); - if (result.state === PreloadState.Error) { - console.error(result.error); - } - return result; - })); + if (source === 'kernel') { + const promise = runPreload(uri, originalUri); + queue = queue.then(() => promise.then(async module => { + try { + await module.activate(); + return { state: PreloadState.Ok }; + } catch (error) { + console.error(error); + return { state: PreloadState.Error, error: error.toString() }; + } + })); + } else { + queue = queue.then(async () => { + try { + await renderers.load(source.rendererId); + return { state: PreloadState.Ok }; + } catch (error) { + console.error(error); + return { state: PreloadState.Error, error: error.toString() }; + } + }); + } + preloadPromises.set(uri, queue); } break; @@ -789,51 +844,88 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } }); - interface MarkupRenderer { - renderMarkup: (context: { element: HTMLElement, content: string }) => void; + interface RendererApi { + renderCell: (id: string, context: ICreateCellInfo) => void; } - const markupRenderers = new class { + class Renderer { + constructor( + public readonly data: RendererMetadata, + private readonly loadDependency: (id: string) => Promise, + ) { } - private readonly mimeTypesToRenderers = new Map Promise; - }>(); + private _loadPromise: Promise | undefined; + private _api: RendererApi | undefined; + + public get api() { return this._api; } + + public load(): Promise { + if (!this._loadPromise) { + this._loadPromise = Promise.all(this.data.dependencies.map(dependencyId => this.loadDependency(dependencyId))) + .then(() => runRenderScript(this.data.entrypoint, this.data.id)) + .then(module => { + if (module) { + const api = module.activate(createRendererContext(this.data.id)); + this._api = api; + return api; + } + return undefined; + }); + } + return this._loadPromise; + } + } + + const renderers = new class { + + private readonly _renderers = new Map(); constructor() { for (const renderer of rendererData) { - let loadPromise: Promise | undefined; - - const entry = { - load: () => { - if (!loadPromise) { - loadPromise = __import(renderer.entrypoint).then(module => { - return module.activate({ dependencies: renderer.dependencies }); - }); - } - return loadPromise; - }, - renderer: undefined, - }; - - for (const mime of renderer.mimeTypes || []) { - if (!this.mimeTypesToRenderers.has(mime)) { - this.mimeTypesToRenderers.set(mime, entry); + this._renderers.set(renderer.id, new Renderer(renderer, async (dependencyId) => { + const parent = this._renderers.get(dependencyId); + if (!parent) { + throw new Error(`Could not find renderer dependency: ${dependencyId}`); } - } + await parent.load(); + })); } } - async renderMarkdown(element: HTMLElement, content: string): Promise { - const entry = this.mimeTypesToRenderers.get('text/markdown'); - if (!entry) { + public getRenderer(id: string): RendererApi | undefined { + return this._renderers.get(id)?.api; + } + + public load(id: string) { + const renderer = this._renderers.get(id); + if (!renderer) { throw new Error('Could not find renderer'); } - const renderer = await entry.load(); - renderer.renderMarkup({ element, content }); + + return renderer.load(); + } + + public async renderMarkdown(id: string, element: HTMLElement, content: string): Promise { + const markdownRenderers = Array.from(this._renderers.values()) + .filter(renderer => renderer.data.mimeTypes.includes('text/markdown')); + + if (!markdownRenderers.length) { + throw new Error('Could not find renderer'); + } + + await Promise.all(markdownRenderers.map(x => x.load())); + + const renderer = Array.from(this._renderers.values()).find(x => x.data.mimeTypes.includes('text/markdown')); + renderer?.api?.renderCell(id, { + element, + value: content, + mime: 'text/markdown', + metadata: undefined, + outputId: undefined, + }); } }(); - vscode.postMessage({ __vscode_notebook_message: true, type: 'initialized' @@ -961,7 +1053,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv previewNode.innerText = ''; } else { previewContainerNode.classList.remove('emptyMarkdownCell'); - await markupRenderers.renderMarkdown(previewNode, content); + await renderers.renderMarkdown(cellId, previewNode, content); if (!hasPostedRenderedMathTelemetry) { const hasRenderedMath = previewNode.querySelector('.katex'); @@ -1060,13 +1152,14 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv }(); } -export interface WebviewPreloadRenderer { +export interface RendererMetadata { + readonly id: string; readonly entrypoint: string; + readonly dependencies: readonly string[] readonly mimeTypes: readonly string[]; - readonly dependencies: ReadonlyArray<{ entrypoint: string }>; } -export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly WebviewPreloadRenderer[]) { +export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly RendererMetadata[]) { // TS will try compiling `import()` in webviePreloads, so use an helper function instead // of using `import(...)` directly return ` From 3dec3b3385a2f01dbba31ccfd30bc6c3cbc49dab Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Thu, 13 May 2021 12:30:10 -0600 Subject: [PATCH 008/270] applied PR feedback --- .../src/features/preview.ts | 27 +++++-- .../src/features/previewManager.ts | 13 ++-- .../src/util/topmostLineMonitor.ts | 70 +++++++++++-------- 3 files changed, 69 insertions(+), 41 deletions(-) diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 71162b60e8f..0254ed42e40 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -12,7 +12,7 @@ import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; import { normalizeResource, WebviewResourceProvider } from '../util/resources'; -import { getVisibleLine, scrollEditorToLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; +import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider'; import { MarkdownEngine } from '../markdownEngine'; @@ -286,6 +286,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { }); } + + private async updatePreview(forceUpdate?: boolean): Promise { clearTimeout(this.throttleTimer); this.throttleTimer = undefined; @@ -529,8 +531,9 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown this._register(this._webviewPanel.onDidChangeViewState(e => { this._onDidChangeViewState.fire(e); })); + this._register(this.preview.onScroll((scrollInfo) => { - this._onScrollEmitter.fire(scrollInfo); + topmostLineMonitor.setPreviousStaticEditorLine(scrollInfo); })); this._register(topmostLineMonitor.onDidChanged(event => { @@ -540,7 +543,7 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown })); const currentLine = this.preview.state.line ? this.preview.state.line : 0; - this._onScrollEmitter.fire({ line: currentLine, uri: this.preview.resource }); + topmostLineMonitor.setPreviousStaticEditorLine({ line: currentLine, uri: this.preview.resource }); } private readonly _onDispose = this._register(new vscode.EventEmitter()); @@ -549,9 +552,6 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown private readonly _onDidChangeViewState = this._register(new vscode.EventEmitter()); public readonly onDidChangeViewState = this._onDidChangeViewState.event; - private readonly _onScrollEmitter = this._register(new vscode.EventEmitter()); - public readonly onScroll = this._onScrollEmitter.event; - override dispose() { this._onDispose.fire(); super.dispose(); @@ -804,3 +804,18 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow } } +/** + * Change the top-most visible line of `editor` to be at `line` + */ +export function scrollEditorToLine( + line: number, + editor: vscode.TextEditor +) { + const sourceLine = Math.floor(line); + const fraction = line - sourceLine; + const text = editor.document.lineAt(sourceLine).text; + const start = Math.floor(fraction * text.length); + editor.revealRange( + new vscode.Range(sourceLine, start, sourceLine + 1, 0), + vscode.TextEditorRevealType.AtTop); +} diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 7b94522808e..738286d2091 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -9,7 +9,7 @@ import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable, disposeAll } from '../util/dispose'; import { TopmostLineMonitor } from '../util/topmostLineMonitor'; -import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview } from './preview'; +import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview, scrollEditorToLine } from './preview'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider } from './previewContentProvider'; @@ -75,6 +75,10 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview super(); this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this)); this._register(vscode.window.registerCustomEditorProvider(this.customEditorViewType, this)); + + this._register(this._topmostLineMonitor.onEditorNeedsScrolling(event => { + scrollEditorToLine(event.line, event.editor); + })); } public refresh() { @@ -160,7 +164,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview document: vscode.TextDocument, webview: vscode.WebviewPanel ): Promise { - const lineNumber = this._topmostLineMonitor.previousMDTextEditor?.document.uri.toString() === document.uri.toString() ? this._topmostLineMonitor.previousMDTextEditor?.visibleRanges[0].start.line : undefined; + const lineNumber = this._topmostLineMonitor.getPreviousMDTextEditorLineByUri(document.uri); const preview = StaticMarkdownPreview.revive( document.uri, webview, @@ -224,11 +228,6 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview this._staticPreviews.delete(preview); }); - // Continuously update the scroll info in case user changes the editor. - preview.onScroll((scrollInfo) => { - this._topmostLineMonitor.previousStaticEditorInfo = scrollInfo; - }); - this.trackActive(preview); return preview; } diff --git a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index 0b66b92cfc1..b2382b46fe2 100644 --- a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -9,19 +9,30 @@ import { isMarkdownFile } from './file'; export interface LastScrollLocation { readonly line: number; - readonly uri: vscode.Uri | undefined; + readonly uri: vscode.Uri; } export class TopmostLineMonitor extends Disposable { private readonly pendingUpdates = new Map(); private readonly throttle = 50; - public previousMDTextEditor: vscode.TextEditor | undefined; - public previousStaticEditorInfo: LastScrollLocation = { line: 0, uri: undefined }; + private previousMDTextEditors = new Map(); + private previousStaticEditorInfo = new Map(); + private isPrevEditorCustom = false; + + private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); + public readonly onDidChanged = this._onChanged.event; + + private readonly _onEditorNeedsScrolling = this._register(new vscode.EventEmitter<{ readonly line: number, readonly editor: vscode.TextEditor }>()); + public readonly onEditorNeedsScrolling = this._onEditorNeedsScrolling.event; constructor() { super(); - this.previousMDTextEditor = vscode.window.activeTextEditor; + + if (vscode.window.activeTextEditor) { + this.setPreviousMDTextEditorLine(vscode.window.activeTextEditor); + } + this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => { if (isMarkdownFile(event.textEditor.document)) { const line = getVisibleLine(event.textEditor); @@ -33,22 +44,40 @@ export class TopmostLineMonitor extends Disposable { this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => { - // When at a markdown file, apply existing scroll settings from static preview if applicable. + // When at a markdown file, apply existing scroll settings from static preview if last editor was custom. // Also save reference to text editor for line number reference later if (textEditor && isMarkdownFile(textEditor.document!)) { - - if (this.previousStaticEditorInfo.uri?.toString() === textEditor.document.uri.toString()) { - const line = this.previousStaticEditorInfo.line ? this.previousStaticEditorInfo.line : 0; - scrollEditorToLine(line, textEditor); + if (this.isPrevEditorCustom) { + const line = this.getPreviousStaticEditorLineByUri(textEditor.document.uri); + if (line) { + this._onEditorNeedsScrolling.fire({ line: line, editor: textEditor }); + } } - - this.previousMDTextEditor = textEditor; + this.setPreviousMDTextEditorLine(textEditor); } + + this.isPrevEditorCustom = (textEditor === undefined); })); } - private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); - public readonly onDidChanged = this._onChanged.event; + public setPreviousMDTextEditorLine(editor: vscode.TextEditor) { + const uri = editor.document.uri; + this.previousMDTextEditors.set(uri.toString(), editor); + } + + public getPreviousMDTextEditorLineByUri(resource: vscode.Uri) { + const editor = this.previousMDTextEditors.get(resource.toString()); + return editor?.visibleRanges[0].start.line; + } + + public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation) { + this.previousStaticEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); + } + + public getPreviousStaticEditorLineByUri(resource: vscode.Uri) { + const scrollLoc = this.previousStaticEditorInfo.get(resource.toString()); + return scrollLoc?.line; + } private updateLine( resource: vscode.Uri, @@ -91,18 +120,3 @@ export function getVisibleLine( const progress = firstVisiblePosition.character / (line.text.length + 2); return lineNumber + progress; } -/** - * Change the top-most visible line of `editor` to be at `line` - */ -export function scrollEditorToLine( - line: number, - editor: vscode.TextEditor -) { - const sourceLine = Math.floor(line); - const fraction = line - sourceLine; - const text = editor.document.lineAt(sourceLine).text; - const start = Math.floor(fraction * text.length); - editor.revealRange( - new vscode.Range(sourceLine, start, sourceLine + 1, 0), - vscode.TextEditorRevealType.AtTop); -} From 9731995d9466c9ae6d51184514de0da765399475 Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Thu, 13 May 2021 12:39:52 -0600 Subject: [PATCH 009/270] addressed ! nit --- .../markdown-language-features/src/util/topmostLineMonitor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index b2382b46fe2..0788b65a31f 100644 --- a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -46,7 +46,7 @@ export class TopmostLineMonitor extends Disposable { // When at a markdown file, apply existing scroll settings from static preview if last editor was custom. // Also save reference to text editor for line number reference later - if (textEditor && isMarkdownFile(textEditor.document!)) { + if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) { if (this.isPrevEditorCustom) { const line = this.getPreviousStaticEditorLineByUri(textEditor.document.uri); if (line) { From 94498778fb1b5a2830276ddedc630e3b11efa4fd Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Thu, 13 May 2021 15:24:56 -0600 Subject: [PATCH 010/270] cleanup --- .../src/util/topmostLineMonitor.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index 0788b65a31f..ad89388c3c7 100644 --- a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -20,12 +20,6 @@ export class TopmostLineMonitor extends Disposable { private previousStaticEditorInfo = new Map(); private isPrevEditorCustom = false; - private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); - public readonly onDidChanged = this._onChanged.event; - - private readonly _onEditorNeedsScrolling = this._register(new vscode.EventEmitter<{ readonly line: number, readonly editor: vscode.TextEditor }>()); - public readonly onEditorNeedsScrolling = this._onEditorNeedsScrolling.event; - constructor() { super(); @@ -60,6 +54,12 @@ export class TopmostLineMonitor extends Disposable { })); } + private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); + public readonly onDidChanged = this._onChanged.event; + + private readonly _onEditorNeedsScrolling = this._register(new vscode.EventEmitter<{ readonly line: number, readonly editor: vscode.TextEditor }>()); + public readonly onEditorNeedsScrolling = this._onEditorNeedsScrolling.event; + public setPreviousMDTextEditorLine(editor: vscode.TextEditor) { const uri = editor.document.uri; this.previousMDTextEditors.set(uri.toString(), editor); From 89b4b6c90d1abdc95e9007112ca42e5982150dac Mon Sep 17 00:00:00 2001 From: Daniel Gary Date: Fri, 14 May 2021 09:18:55 -0500 Subject: [PATCH 011/270] Replaced wmic call with windows-process-tree --- src/vs/workbench/contrib/debug/node/terminals.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 7b3326f6e22..be1f1d93e4b 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -12,6 +12,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; + function spawnAsPromised(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { let stdout = ''; @@ -50,14 +51,19 @@ export function runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestAr export function hasChildProcesses(processId: number | undefined): Promise { if (processId) { + // if shell has at least one child process, assume that shell is busy if (platform.isWindows) { - return spawnAsPromised('wmic', ['process', 'get', 'ParentProcessId']).then(stdout => { - const pids = stdout.split('\r\n'); - return pids.some(p => parseInt(p) === processId); - }, error => { - return true; + + return new Promise(async (resolve) => { + + const windowsProcessTree = await import('windows-process-tree'); + windowsProcessTree.getProcessTree(processId, (processTree) => { + resolve(processTree.children.length > 0); + }); + }); + } else { return spawnAsPromised('/usr/bin/pgrep', ['-lP', String(processId)]).then(stdout => { const r = stdout.trim(); From 09b77ba6d8b71ba9ec32eff2abd070366c614ecd Mon Sep 17 00:00:00 2001 From: Daniel Gary Date: Fri, 14 May 2021 09:20:50 -0500 Subject: [PATCH 012/270] added comment and removed empty lines --- src/vs/workbench/contrib/debug/node/terminals.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index be1f1d93e4b..a4ef4b4c84f 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -54,16 +54,13 @@ export function hasChildProcesses(processId: number | undefined): Promise(async (resolve) => { - + // See #123296 const windowsProcessTree = await import('windows-process-tree'); windowsProcessTree.getProcessTree(processId, (processTree) => { resolve(processTree.children.length > 0); }); - }); - } else { return spawnAsPromised('/usr/bin/pgrep', ['-lP', String(processId)]).then(stdout => { const r = stdout.trim(); From dc85211b15988acd5a567477166ceda3693cb793 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 17 May 2021 11:44:00 +0200 Subject: [PATCH 013/270] untitled file working copy - first cut --- .../common/untitledTextEditorModel.ts | 75 ++--- .../test/browser/untitledTextEditor.test.ts | 16 - .../workingCopy/common/fileWorkingCopy.ts | 10 +- .../common/untitledFileWorkingCopy.ts | 281 ++++++++++++++++++ 4 files changed, 319 insertions(+), 63 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 732985245f5..7f4557bdde7 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ITextModel } from 'vs/editor/common/model'; -import { createTextBufferFactory, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; +import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -63,12 +63,6 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport * Resolves the untitled model. */ resolve(): Promise; - - /** - * Updates the value of the untitled model optionally allowing to ignore dirty. - * The model must be resolved for this method to work. - */ - setValue(value: string, ignoreDirty?: boolean): void; } export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel { @@ -99,6 +93,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt readonly capabilities = WorkingCopyCapabilities.Untitled; + //#region Name + + private configuredLabelFormat: 'content' | 'name' = 'content'; + private cachedModelFirstLineWords: string | undefined = undefined; get name(): string { // Take name from first line if present and only if @@ -112,13 +110,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.labelService.getUriBasenameLabel(this.resource); } - private dirty = this.hasAssociatedFilePath || !!this.initialValue; - private ignoreDirtyOnModelContentChange = false; + //#endregion - private versionId = 0; - - private configuredEncoding: string | undefined; - private configuredLabelFormat: 'content' | 'name' = 'content'; constructor( public readonly resource: URI, @@ -153,7 +146,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private registerListeners(): void { // Config Changes - this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.onConfigurationChange(true))); + this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => this.onConfigurationChange(true))); } private onConfigurationChange(fromEvent: boolean): void { @@ -179,9 +172,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } } - getVersionId(): number { - return this.versionId; - } + + //#region Mode private _hasModeSetExplicitly: boolean = false; get hasModeSetExplicitly(): boolean { return this._hasModeSetExplicitly; } @@ -216,6 +208,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.preferredMode; } + //#endregion + + + //#region Encoding + + private configuredEncoding: string | undefined; + getEncoding(): string | undefined { return this.preferredEncoding || this.configuredEncoding; } @@ -230,25 +229,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } } - setValue(value: string, ignoreDirty?: boolean): void { - if (ignoreDirty) { - this.ignoreDirtyOnModelContentChange = true; - } - - try { - this.updateTextEditorModel(createTextBufferFactory(value)); - } finally { - this.ignoreDirtyOnModelContentChange = false; - } - } - - override isReadonly(): boolean { - return false; - } + //#endregion //#region Dirty + private dirty = this.hasAssociatedFilePath || !!this.initialValue; + isDirty(): boolean { return this.dirty; } @@ -360,19 +347,16 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } private onModelContentChanged(textEditorModel: ITextModel, e: IModelContentChangedEvent): void { - this.versionId++; - if (!this.ignoreDirtyOnModelContentChange) { - // mark the untitled text editor as non-dirty once its content becomes empty and we do - // not have an associated path set. we never want dirty indicator in that case. - if (!this.hasAssociatedFilePath && textEditorModel.getLineCount() === 1 && textEditorModel.getLineContent(1) === '') { - this.setDirty(false); - } + // mark the untitled text editor as non-dirty once its content becomes empty and we do + // not have an associated path set. we never want dirty indicator in that case. + if (!this.hasAssociatedFilePath && textEditorModel.getLineCount() === 1 && textEditorModel.getLineContent(1) === '') { + this.setDirty(false); + } - // turn dirty otherwise - else { - this.setDirty(true); - } + // turn dirty otherwise + else { + this.setDirty(true); } // Check for name change if first line changed in the range of 0-FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH columns @@ -421,4 +405,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } //#endregion + + + override isReadonly(): boolean { + return false; + } } diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index e3abf1938ea..2eea7d8a629 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -102,22 +102,6 @@ suite('Untitled text editors', () => { }); } - test('setValue()', async () => { - const service = accessor.untitledTextEditorService; - const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - - const model = await untitled.resolve(); - - model.setValue('not dirty', true); - assert.ok(!model.isDirty()); - - model.setValue('dirty'); - assert.ok(model.isDirty()); - - untitled.dispose(); - model.dispose(); - }); - test('associated resource is dirty', async () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index 5d573428ee8..ba40ccd1df1 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -445,6 +445,10 @@ export class FileWorkingCopy extends ResourceWo private lastResolvedFileStat: IFileStatWithMetadata | undefined; + isResolved(): this is IResolvedFileWorkingCopy { + return !!this.model; + } + async resolve(options?: IFileWorkingCopyResolveOptions): Promise { this.trace('[file working copy] resolve() - enter'); @@ -1172,6 +1176,8 @@ export class FileWorkingCopy extends ResourceWo return; // ignore if not resolved or not dirty and not enforced } + this.trace('[file working copy] revert()'); + // Unset flags const wasDirty = this.dirty; const undoSetDirty = this.doSetDirty(false); @@ -1235,10 +1241,6 @@ export class FileWorkingCopy extends ResourceWo //#region Utilities - isResolved(): this is IResolvedFileWorkingCopy { - return !!this.model; - } - isReadonly(): boolean { return this.lastResolvedFileStat?.readonly || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts new file mode 100644 index 00000000000..02ed75d2d8e --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -0,0 +1,281 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ISaveOptions } from 'vs/workbench/common/editor'; +import { raceCancellation } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { emptyStream } from 'vs/base/common/stream'; + +export interface IUntitledFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } + +export interface IUntitledFileWorkingCopyModel extends IFileWorkingCopyModel { + readonly onDidChangeContent: Event; +} + +export interface IUntitledFileWorkingCopyModelContentChangedEvent extends IFileWorkingCopyModelContentChangedEvent { + + /** + * Flag that indicates that the content change + * resulted in empty contents. A untitled file + * working copy without contents may be marked + * as non-dirty. + */ + readonly isEmpty: boolean; +} + +export interface IUntitledFileWorkingCopy extends IWorkingCopy { + + /** + * Emits an event when this untitled model is reverted. + */ + readonly onDidRevert: Event; + + /** + * Provides access to the underlying model of this untitled + * file based working copy. As long as the untitled file working + * copy has not been resolved, the model is `undefined`. + */ + readonly model: T | undefined; + + /** + * Whether this untitled file working copy model has an associated file path. + */ + readonly hasAssociatedFilePath: boolean; + + /** + * Resolves an untitled file working copy. + */ + resolve(): Promise; +} + +export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { + + /** + * A resolved untitled file working copy has a resolved model `T`. + */ + readonly model: T; +} + +export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { + + readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.Untitled; + + private _model: T | undefined = undefined; + get model(): T | undefined { return this._model; } + + //#region Events + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidRevert = this._register(new Emitter()); + readonly onDidRevert = this._onDidRevert.event; + + //#endregion + + constructor( + readonly typeId: string, + readonly resource: URI, + readonly name: string, + readonly hasAssociatedFilePath: boolean, + private readonly initialValue: VSBufferReadableStream | undefined, + private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @ILogService private readonly logService: ILogService + ) { + super(); + + if (resource.scheme !== Schemas.untitled) { + throw new Error(`The untitled file working copy resource ${this.resource.toString(true)} is not using untitled as scheme.`); + } + + // Make known to working copy service + this._register(workingCopyService.registerWorkingCopy(this)); + } + + //#region Dirty + + private dirty = this.hasAssociatedFilePath || !!this.initialValue; + + isDirty(): boolean { + return this.dirty; + } + + private setDirty(dirty: boolean): void { + if (this.dirty === dirty) { + return; + } + + this.dirty = dirty; + this._onDidChangeDirty.fire(); + } + + //#endregion + + + //#region Resolve + + async resolve(): Promise { + this.trace('[untitled file working copy] resolve()'); + + if (this.isResolved()) { + this.trace('[untitled file working copy] resolve() - exit (already resolved)'); + + // return early if the untitled file working copy is already + // resolved assuming that the contents have meanwhile changed + // in the underlying model. we only resolve untitled once. + return; + } + + let untitledContents: VSBufferReadableStream; + + // Check for backups or use initial value or empty + const backup = await this.workingCopyBackupService.resolve(this); + if (backup) { + this.trace('[untitled file working copy] resolve() - with backup'); + + untitledContents = backup.value; + } else if (this.initialValue) { + this.trace('[untitled file working copy] resolve() - with initial contents'); + + untitledContents = this.initialValue; + } else { + this.trace('[untitled file working copy] resolve() - empty'); + + untitledContents = emptyStream(); + } + + // Create model + await this.doCreateModel(untitledContents); + + // Untitled associated to file path are dirty right away as well as untitled with content + this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue); + + // If we have initial contents, make sure to emit this + // as the appropiate events to the outside. + if (!!backup || this.initialValue) { + this._onDidChangeContent.fire(); + } + } + + private async doCreateModel(contents: VSBufferReadableStream): Promise { + this.trace('[untitled file working copy] doCreateModel()'); + + // Create model and dispose it when we get disposed + this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); + + // Model listeners + this.installModelListeners(this._model); + } + + private installModelListeners(model: IUntitledFileWorkingCopyModel): void { + + // Content Change + this._register(model.onDidChangeContent(e => this.onModelContentChanged(e))); + + // Lifecycle + this._register(model.onWillDispose(() => this.dispose())); + } + + private onModelContentChanged(e: IUntitledFileWorkingCopyModelContentChangedEvent): void { + + // Mark the untitled file working copy as non-dirty once its + // content becomes empty and we do not have an associated + // path set. we never want dirty indicator in that case. + if (!this.hasAssociatedFilePath && e.isEmpty) { + this.setDirty(false); + } + + // Turn dirty otherwise + else { + this.setDirty(true); + } + + // Emit as general content change event + this._onDidChangeContent.fire(); + } + + isResolved(): this is IResolvedUntitledFileWorkingCopy { + return !!this.model; + } + + //#endregion + + + //#region Backup + + async backup(token: CancellationToken): Promise { + + // Fill in content if we are resolved + let content: VSBufferReadableStream | undefined = undefined; + if (this.isResolved()) { + content = await raceCancellation(this.model.snapshot(token), token); + } + + return { content }; + } + + //#endregion + + + //#region Save + + save(options?: ISaveOptions): Promise { + this.trace('[untitled file working copy] save() - enter'); + + // TODO needs to extract the code for bringing up save dialog + // or use the associated file path as target + // Also, who is disposing the untitled after save and open the + // new editor? + + throw new Error('Method not implemented.'); + } + + //#endregion + + + //#region Revert + + async revert(): Promise { + this.trace('[untitled file working copy] revert()'); + + // No longer dirty + this.setDirty(false); + + // Emit as event + this._onDidRevert.fire(); + + // A reverted untitled file working copy is invalid + // because it has no actual source on disk to revert to. + // As such we dispose the model. + this.dispose(); + } + + //#endregion + + override dispose(): void { + this.trace('[untitled file working copy] dispose()'); + + super.dispose(); + } + + private trace(msg: string): void { + this.logService.trace(msg, this.resource.toString(true), this.typeId); + } +} From f2640c349b0851f5bde9764a46515dd35fbfc1b0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 17 May 2021 13:59:48 +0200 Subject: [PATCH 014/270] untitled file working copy - first cut manager --- .../common/fileWorkingCopyManager.ts | 11 +- .../common/untitledFileWorkingCopy.ts | 14 +- .../common/untitledFileWorkingCopyManager.ts | 308 ++++++++++++++++++ 3 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index a3ba3b249dc..72a7e3f3c5e 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -472,10 +472,17 @@ export class FileWorkingCopyManager extends Dis else { didCreateWorkingCopy = true; - const newWorkingCopy = workingCopy = this.instantiationService.createInstance(FileWorkingCopy, this.workingCopyTypeId, resource, this.labelService.getUriBasenameLabel(resource), this.modelFactory) as unknown as IFileWorkingCopy; + workingCopy = this.instantiationService.createInstance( + FileWorkingCopy, + this.workingCopyTypeId, + resource, + this.labelService.getUriBasenameLabel(resource), + this.modelFactory + ) as unknown as IFileWorkingCopy; + workingCopyResolve = workingCopy.resolve(options); - this.registerWorkingCopy(newWorkingCopy); + this.registerWorkingCopy(workingCopy); } // Store pending resolve to avoid race conditions diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 02ed75d2d8e..670517c603f 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -7,7 +7,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -35,13 +35,18 @@ export interface IUntitledFileWorkingCopyModelContentChangedEvent extends IFileW readonly isEmpty: boolean; } -export interface IUntitledFileWorkingCopy extends IWorkingCopy { +export interface IUntitledFileWorkingCopy extends IWorkingCopy, IDisposable { /** * Emits an event when this untitled model is reverted. */ readonly onDidRevert: Event; + /** + * An event for when the file working copy has been disposed. + */ + readonly onWillDispose: Event; + /** * Provides access to the underlying model of this untitled * file based working copy. As long as the untitled file working @@ -86,6 +91,9 @@ export class UntitledFileWorkingCopy ex private readonly _onDidRevert = this._register(new Emitter()); readonly onDidRevert = this._onDidRevert.event; + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + //#endregion constructor( @@ -272,6 +280,8 @@ export class UntitledFileWorkingCopy ex override dispose(): void { this.trace('[untitled file working copy] dispose()'); + this._onWillDispose.fire(); + super.dispose(); } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts new file mode 100644 index 00000000000..3a5c980a50d --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -0,0 +1,308 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { Promises } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IFileService } from 'vs/platform/files/common/files'; + +export interface INewUntitledFileWorkingCopyOptions { + + /** + * Initial value of the untitled file working copy. + * + * Note: An untitled file working copy with initial + * value is dirty right from the beginning. + */ + initialValue?: VSBufferReadableStream; +} + +export interface INewUntitledFileWorkingCopyWithAssociatedResourceOptions extends INewUntitledFileWorkingCopyOptions { + + /** + * Resource components to associate with the untitled file working copy. + * When saving, the associated components will be used and the user + * is not being asked to provide a file path. + * + * Note: currently it is not possible to specify the `scheme` to use. The + * untitled file working copy will saved to the default local or remote resource. + */ + associatedResource: { authority: string; path: string; query: string; fragment: string; } +} + +export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFileWorkingCopyOptions { + + /** + * A resource to identify the untitled file working copy + * to create or return if already existing. + * + * Note: the resource will not be used unless the scheme is `untitled`. + */ + untitledResource: URI; +} + +/** + * The only one that should be dealing with `IUntitledFileWorkingCopy` and + * handle all operations that are working copy related, such as save/revert, + * backup and resolving. + */ +export interface IUntitledFileWorkingCopyManager extends IDisposable { + + /** + * An event for when a untitled file working copy changed it's dirty state. + */ + readonly onDidChangeDirty: Event>; + + /** + * An event for when a untitled file working copy is about to be disposed. + */ + readonly onWillDispose: Event>; + + /** + * Access to all known untitled file working copies within the manager. + */ + readonly workingCopies: readonly IUntitledFileWorkingCopy[]; + + /** + * Returns an existing untitled file working copy if already created before + * or `undefined` otherwise. + */ + get(resource: URI): IUntitledFileWorkingCopy | undefined; + + /** + * Resolves an untitled file working copy from the provided options. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Resolves an untitled file working copy from the provided options + * unless an existing working copy already exists with that resource. + */ + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + + /** + * Disposes all working copies of the manager and disposes the manager. This + * method is different from `dispose` in that it will unregister any working + * copy from the `IWorkingCopyService`. Since this impact things like backups, + * the method is `async` because it needs to trigger `save` for any dirty + * working copy to preserve the data. + * + * Callers should make sure to e.g. close any editors associated with the + * working copy. + */ + destroy(): Promise; +} + +type IInternalUntitledFileWorkingCopyOptions = IExistingUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions; + +export class UntitledFileWorkingCopyManager extends Disposable implements IUntitledFileWorkingCopyManager { + + //#region Events + + private readonly _onDidChangeDirty = this._register(new Emitter>()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onWillDispose = this._register(new Emitter>()); + readonly onWillDispose = this._onWillDispose.event; + + //#endregion + + private readonly mapResourceToWorkingCopy = new ResourceMap>(); + + constructor( + private readonly workingCopyTypeId: string, + private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILabelService private readonly labelService: ILabelService, + @ILogService private readonly logService: ILogService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @IFileService private readonly fileService: IFileService + ) { + super(); + } + + //#region Get / Get all + + get workingCopies(): IUntitledFileWorkingCopy[] { + return [...this.mapResourceToWorkingCopy.values()]; + } + + get(resource: URI): IUntitledFileWorkingCopy | undefined { + return this.mapResourceToWorkingCopy.get(resource); + } + + //#endregion + + //#region Resolve / Create + + async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { + const workingCopy = this.doCreateOrGet(options); + await workingCopy.resolve(); + + return workingCopy; + } + + private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { + const massagedOptions = this.massageOptions(options); + + // Return existing instance if asked for it + if (massagedOptions.untitledResource && this.mapResourceToWorkingCopy.has(massagedOptions.untitledResource)) { + return this.mapResourceToWorkingCopy.get(massagedOptions.untitledResource)!; + } + + // Create new instance otherwise + return this.doCreate(massagedOptions); + } + + private massageOptions(options: IInternalUntitledFileWorkingCopyOptions): IInternalUntitledFileWorkingCopyOptions { + const massagedOptions: IInternalUntitledFileWorkingCopyOptions = Object.create(null); + + // Figure out associated and untitled resource + if (options.associatedResource) { + massagedOptions.untitledResource = URI.from({ + scheme: Schemas.untitled, + authority: options.associatedResource.authority, + fragment: options.associatedResource.fragment, + path: options.associatedResource.path, + query: options.associatedResource.query + }); + massagedOptions.associatedResource = options.associatedResource; + } else { + if (options.untitledResource?.scheme === Schemas.untitled) { + massagedOptions.untitledResource = options.untitledResource; + } + } + + // Take over initial value + massagedOptions.initialValue = options.initialValue; + + return massagedOptions; + } + + private doCreate(options: IInternalUntitledFileWorkingCopyOptions): IUntitledFileWorkingCopy { + + // Create a new untitled resource if none is provided + let untitledResource = options.untitledResource; + if (!untitledResource) { + let counter = 1; + do { + untitledResource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` }); + counter++; + } while (this.mapResourceToWorkingCopy.has(untitledResource)); + } + + // Create new working copy with provided options + const workingCopy = this.instantiationService.createInstance( + UntitledFileWorkingCopy, + this.workingCopyTypeId, + untitledResource, + this.labelService.getUriBasenameLabel(untitledResource), + !!options.associatedResource, + options.initialValue, + this.modelFactory + ) as unknown as IUntitledFileWorkingCopy; + + this.registerWorkingCopy(workingCopy); + + return workingCopy; + } + + private registerWorkingCopy(workingCopy: IUntitledFileWorkingCopy): void { + + // Install working copy listeners + const listeners = new DisposableStore(); + listeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); + listeners.add(workingCopy.onWillDispose(() => this._onWillDispose.fire(workingCopy))); + + // Remove from cache on dispose + Event.once(workingCopy.onWillDispose)(() => { + + // Registry + this.mapResourceToWorkingCopy.delete(workingCopy.resource); + + // Listeners + listeners.dispose(); + }); + + // Add to cache + this.mapResourceToWorkingCopy.set(workingCopy.resource, workingCopy); + + // If the working copy is dirty right from the beginning, + // make sure to emit this as an event + if (workingCopy.isDirty()) { + this._onDidChangeDirty.fire(workingCopy); + } + } + + //#endregion + + //#region Lifecycle + + override dispose(): void { + super.dispose(); + + // Clear working copy caches + // + // Note: we are not explicitly disposing the working copies + // known to the manager because this can have unwanted side + // effects such as backups getting discarded once the working + // copy unregisters. We have an explicit `destroy` + // for that purpose (https://github.com/microsoft/vscode/pull/123555) + // + this.mapResourceToWorkingCopy.clear(); + } + + async destroy(): Promise { + const workingCopies = Array.from(this.mapResourceToWorkingCopy.values()); + + // Make sure all dirty working copies are saved to disk + try { + await Promises.settled(workingCopies.map(async workingCopy => { + if (workingCopy.isDirty()) { + await this.saveWithFallback(workingCopy); + } + })); + } catch (error) { + this.logService.error(error); + } + + // Dispose all working copies + dispose(workingCopies); + + // Finally dispose manager + this.dispose(); + } + + private async saveWithFallback(workingCopy: IUntitledFileWorkingCopy): Promise { + + // First try regular save + let saveFailed = false; + try { + await workingCopy.save(); + } catch (error) { + saveFailed = true; + } + + // Then fallback to backup if that exists + if (saveFailed || workingCopy.isDirty()) { + const backup = await this.workingCopyBackupService.resolve(workingCopy); + if (backup) { + await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); + } + } + + //#endregion + } +} From 63b6e6c51d783a184a8d7a9315e5f36dfbb70ca1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 17 May 2021 15:37:17 +0200 Subject: [PATCH 015/270] untitled file working copy - tests --- .../common/untitledFileWorkingCopy.ts | 6 +- .../common/untitledFileWorkingCopyManager.ts | 4 +- .../test/browser/fileWorkingCopy.test.ts | 8 + .../browser/untitledFileWorkingCopy.test.ts | 311 ++++++++++++++++++ .../untitledFileWorkingCopyManager.test.ts | 146 ++++++++ 5 files changed, 471 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts create mode 100644 src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 670517c603f..0d59f0fa23d 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -244,7 +244,7 @@ export class UntitledFileWorkingCopy ex //#region Save - save(options?: ISaveOptions): Promise { + async save(options?: ISaveOptions): Promise { this.trace('[untitled file working copy] save() - enter'); // TODO needs to extract the code for bringing up save dialog @@ -252,7 +252,9 @@ export class UntitledFileWorkingCopy ex // Also, who is disposing the untitled after save and open the // new editor? - throw new Error('Method not implemented.'); + await this.revert(); + + return true; } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 3a5c980a50d..3585096df18 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -38,7 +38,7 @@ export interface INewUntitledFileWorkingCopyWithAssociatedResourceOptions extend * Note: currently it is not possible to specify the `scheme` to use. The * untitled file working copy will saved to the default local or remote resource. */ - associatedResource: { authority: string; path: string; query: string; fragment: string; } + associatedResource: { authority?: string; path?: string; query?: string; fragment?: string; } } export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFileWorkingCopyOptions { @@ -145,7 +145,7 @@ export class UntitledFileWorkingCopyManager> { const workingCopy = this.doCreateOrGet(options); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index 3c0028841b7..47ab58bb6a7 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -112,6 +112,14 @@ suite('FileWorkingCopy', function () { workingCopy.dispose(); }); + test('registers with working copy service', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + + workingCopy.dispose(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + }); + test('requires good file system URI', async () => { assert.throws(() => createWorkingCopy(URI.from({ scheme: 'unknown', path: 'somePath' }))); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts new file mode 100644 index 00000000000..8c7da14566a --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -0,0 +1,311 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { VSBufferReadableStream, newWriteableBufferStream, VSBuffer, streamToBuffer, bufferToStream } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/resources'; +import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +export class TestUntitledFileWorkingCopyModel extends Disposable implements IUntitledFileWorkingCopyModel { + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + constructor(readonly resource: URI, public contents: string) { + super(); + } + + fireContentChangeEvent(event: IUntitledFileWorkingCopyModelContentChangedEvent): void { + this._onDidChangeContent.fire(event); + } + + updateContents(newContents: string): void { + this.doUpdate(newContents); + } + + private throwOnSnapshot = false; + setThrowOnSnapshot(): void { + this.throwOnSnapshot = true; + } + + async snapshot(token: CancellationToken): Promise { + if (this.throwOnSnapshot) { + throw new Error('Fail'); + } + + const stream = newWriteableBufferStream(); + stream.end(VSBuffer.fromString(this.contents)); + + return stream; + } + + async update(contents: VSBufferReadableStream, token: CancellationToken): Promise { + this.doUpdate((await streamToBuffer(contents)).toString()); + } + + private doUpdate(newContents: string): void { + this.contents = newContents; + + this.versionId++; + + this._onDidChangeContent.fire({ isRedoing: false, isUndoing: false, isEmpty: newContents.length === 0 }); + } + + versionId = 0; + + pushedStackElement = false; + + pushStackElement(): void { + this.pushedStackElement = true; + } + + override dispose(): void { + this._onWillDispose.fire(); + + super.dispose(); + } +} + +export class TestUntitledFileWorkingCopyModelFactory implements IUntitledFileWorkingCopyModelFactory { + + async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise { + return new TestUntitledFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString()); + } +} + +suite('UntitledFileWorkingCopy', () => { + + const factory = new TestUntitledFileWorkingCopyModelFactory(); + + let resource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + let workingCopy: UntitledFileWorkingCopy; + + function createWorkingCopy(uri: URI = resource, hasAssociatedFilePath = false, initialValue = '') { + return new UntitledFileWorkingCopy( + 'testUntitledWorkingCopyType', + uri, + basename(uri), + hasAssociatedFilePath, + initialValue.length > 0 ? bufferToStream(VSBuffer.fromString(initialValue)) : undefined, + factory, + accessor.workingCopyService, + accessor.workingCopyBackupService, + accessor.logService + ); + } + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + workingCopy = createWorkingCopy(); + }); + + teardown(() => { + workingCopy.dispose(); + }); + + test('registers with working copy service', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + + workingCopy.dispose(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + }); + + test('requires good untitled URI', async () => { + assert.throws(() => createWorkingCopy(URI.from({ scheme: 'unknown', path: 'somePath' }))); + }); + + test('dirty', async () => { + assert.strictEqual(workingCopy.isDirty(), false); + + let changeDirtyCounter = 0; + workingCopy.onDidChangeDirty(() => { + changeDirtyCounter++; + }); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + assert.strictEqual(workingCopy.isResolved(), true); + + // Dirty from: Model content change + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(contentChangeCounter, 1); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(changeDirtyCounter, 1); + + await workingCopy.save(); + + assert.strictEqual(workingCopy.isDirty(), false); + assert.strictEqual(changeDirtyCounter, 2); + }); + + test('dirty - cleared when content event signals isEmpty', async () => { + assert.strictEqual(workingCopy.isDirty(), false); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + workingCopy.model?.fireContentChangeEvent({ isEmpty: true, isUndoing: false, isRedoing: false }); + + assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('dirty - not cleared when content event signals isEmpty when associated resource', async () => { + workingCopy.dispose(); + workingCopy = createWorkingCopy(resource, true); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + workingCopy.model?.fireContentChangeEvent({ isEmpty: true, isUndoing: false, isRedoing: false }); + + assert.strictEqual(workingCopy.isDirty(), true); + }); + + test('revert', async () => { + let revertCounter = 0; + workingCopy.onDidRevert(() => { + revertCounter++; + }); + + let disposeCounter = 0; + workingCopy.onWillDispose(() => { + disposeCounter++; + }); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + await workingCopy.revert(); + + assert.strictEqual(revertCounter, 1); + assert.strictEqual(disposeCounter, 1); + assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('dispose', async () => { + let disposeCounter = 0; + workingCopy.onWillDispose(() => { + disposeCounter++; + }); + + await workingCopy.resolve(); + workingCopy.dispose(); + + assert.strictEqual(disposeCounter, 1); + }); + + test('backup', async () => { + assert.strictEqual((await workingCopy.backup(CancellationToken.None)).content, undefined); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('Hello Backup'); + const backup = await workingCopy.backup(CancellationToken.None); + + let backupContents: string | undefined = undefined; + if (isReadableStream(backup.content)) { + backupContents = (await consumeStream(backup.content, chunks => VSBuffer.concat(chunks))).toString(); + } else if (backup.content) { + backupContents = consumeReadable(backup.content, chunks => VSBuffer.concat(chunks)).toString(); + } + + assert.strictEqual(backupContents, 'Hello Backup'); + }); + + test('resolve - without contents', async () => { + assert.strictEqual(workingCopy.isResolved(), false); + assert.strictEqual(workingCopy.hasAssociatedFilePath, false); + assert.strictEqual(workingCopy.model, undefined); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isResolved(), true); + assert.ok(workingCopy.model); + }); + + test('resolve - with initial contents', async () => { + workingCopy.dispose(); + + workingCopy = createWorkingCopy(resource, false, 'Hello Initial'); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.model?.contents, 'Hello Initial'); + assert.strictEqual(contentChangeCounter, 1); + + workingCopy.model.updateContents('Changed contents'); + + await workingCopy.resolve(); // second resolve should be ignored + assert.strictEqual(workingCopy.model?.contents, 'Changed contents'); + }); + + test('resolve - with associated resource', async () => { + workingCopy.dispose(); + workingCopy = createWorkingCopy(resource, true); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.hasAssociatedFilePath, true); + }); + + test('resolve - with backup', async () => { + await workingCopy.resolve(); + workingCopy.model?.updateContents('Hello Backup'); + + const backup = await workingCopy.backup(CancellationToken.None); + await accessor.workingCopyBackupService.backup(workingCopy, backup.content, undefined, backup.meta); + + assert.strictEqual(accessor.workingCopyBackupService.hasBackupSync(workingCopy), true); + + workingCopy.dispose(); + + workingCopy = createWorkingCopy(); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.model?.contents, 'Hello Backup'); + assert.strictEqual(contentChangeCounter, 1); + }); +}); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts new file mode 100644 index 00000000000..417fad0cfbe --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; +import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('UntitledFileWorkingCopyManager', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + let manager: IUntitledFileWorkingCopyManager; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + const factory = new TestUntitledFileWorkingCopyModelFactory(); + manager = new UntitledFileWorkingCopyManager('testUntitledWorkingCopyType', factory, instantiationService, accessor.labelService, accessor.logService, accessor.workingCopyBackupService, accessor.fileService); + }); + + teardown(() => { + manager.dispose(); + }); + + test('basics', async () => { + let disposeCounter = 0; + manager.onWillDispose(e => { + disposeCounter++; + }); + + let dirtyCounter = 0; + manager.onDidChangeDirty(e => { + dirtyCounter++; + }); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.workingCopies.length, 0); + + assert.strictEqual(manager.get(URI.file('/some/invalidPath')), undefined); + assert.strictEqual(manager.get(URI.file('/some/invalidPath').with({ scheme: Schemas.untitled })), undefined); + + const workingCopy1 = await manager.resolve(); + const workingCopy2 = await manager.resolve(); + + assert.strictEqual(manager.get(workingCopy1.resource), workingCopy1); + assert.strictEqual(manager.get(workingCopy2.resource), workingCopy2); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); + assert.strictEqual(manager.workingCopies.length, 2); + + assert.notStrictEqual(workingCopy1.resource.toString(), workingCopy2.resource.toString()); + + for (const workingCopy of [workingCopy1, workingCopy2]) { + assert.strictEqual(workingCopy.capabilities, WorkingCopyCapabilities.Untitled); + assert.strictEqual(workingCopy.isDirty(), false); + assert.ok(workingCopy.model); + } + + workingCopy1.model?.updateContents('Hello World'); + + assert.strictEqual(workingCopy1.isDirty(), true); + assert.strictEqual(dirtyCounter, 1); + + workingCopy1.model?.updateContents(''); // change to empty clears dirty flag + assert.strictEqual(workingCopy1.isDirty(), false); + assert.strictEqual(dirtyCounter, 2); + + workingCopy2.model?.fireContentChangeEvent({ isEmpty: false, isRedoing: false, isUndoing: false }); + assert.strictEqual(workingCopy2.isDirty(), true); + assert.strictEqual(dirtyCounter, 3); + + workingCopy1.dispose(); + + assert.strictEqual(manager.workingCopies.length, 1); + assert.strictEqual(manager.get(workingCopy1.resource), undefined); + + workingCopy2.dispose(); + + assert.strictEqual(manager.workingCopies.length, 0); + assert.strictEqual(manager.get(workingCopy2.resource), undefined); + + assert.strictEqual(disposeCounter, 2); + }); + + test('resolve - with initial value', async () => { + let dirtyCounter = 0; + manager.onDidChangeDirty(e => { + dirtyCounter++; + }); + + const workingCopy = await manager.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello World')) }); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(dirtyCounter, 1); + assert.strictEqual(workingCopy.model?.contents, 'Hello World'); + + workingCopy.dispose(); + }); + + test('resolve - existing', async () => { + const workingCopy1 = await manager.resolve(); + + const workingCopy2 = await manager.resolve({ untitledResource: workingCopy1.resource }); + assert.strictEqual(workingCopy1, workingCopy2); + + const workingCopy3 = await manager.resolve({ untitledResource: URI.file('/invalid/untitled') }); + assert.strictEqual(workingCopy3.resource.scheme, Schemas.untitled); + + workingCopy1.dispose(); + workingCopy2.dispose(); + workingCopy3.dispose(); + }); + + test('resolve - with associated resource', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + + assert.strictEqual(workingCopy.hasAssociatedFilePath, true); + assert.strictEqual(workingCopy.resource.path, '/some/associated.txt'); + }); + + test('destroy', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + + await manager.resolve(); + await manager.resolve(); + await manager.resolve(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); + assert.strictEqual(manager.workingCopies.length, 3); + + await manager.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.workingCopies.length, 0); + }); +}); From 9719ae6e725bfb6cca063f555455325ff8cb71b3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 17 May 2021 16:32:14 +0200 Subject: [PATCH 016/270] untitled file working copy - :lipstick: --- .../workingCopy/common/untitledFileWorkingCopyManager.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 3585096df18..674cca7295a 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -158,8 +158,11 @@ export class UntitledFileWorkingCopyManager Date: Mon, 17 May 2021 17:38:03 -0700 Subject: [PATCH 017/270] Fix ctrl+enter in code cell editor --- .../workbench/contrib/notebook/browser/contrib/coreActions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 56d92da0111..059c703ab37 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -1147,7 +1147,9 @@ registerAction2(class QuitEditCellAction extends NotebookCellAction { weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - 5 }, { - when: quitEditCondition, + when: ContextKeyExpr.and( + quitEditCondition, + NOTEBOOK_CELL_TYPE.isEqualTo('markdown')), primary: KeyMod.WinCtrl | KeyCode.Enter, win: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter From 102433ffbef95761b41a403a477a82bee618c07f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 07:58:10 +0200 Subject: [PATCH 018/270] untitled file working copy - extract reusable interfaces --- .../common/abstractFileWorkingCopy.ts | 99 +++++++++++++++++++ .../workingCopy/common/fileWorkingCopy.ts | 82 ++------------- .../common/untitledFileWorkingCopy.ts | 48 ++++----- .../browser/untitledFileWorkingCopy.test.ts | 6 +- .../untitledFileWorkingCopyManager.test.ts | 2 +- 5 files changed, 129 insertions(+), 108 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts new file mode 100644 index 00000000000..36b4f2ac181 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { URI } from 'vs/base/common/uri'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; + +export interface IBaseFileWorkingCopyModelFactory { + + /** + * Create a model from the given content under the provided resource. + * + * @param resource the `URI` of the model + * @param contents the content of the model to create it + * @param token support for cancellation + */ + createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise; +} + +/** + * A generic file working copy model to be reused by untitled + * and titled file working copies. + */ +export interface IBaseFileWorkingCopyModel extends IDisposable { + + /** + * This event signals ANY changes to the contents, for example: + * - through the user typing into the editor + * - from API usage (e.g. bulk edits) + * - when `IBaseFileWorkingCopyModel#update` is invoked with contents + * that are different from the current contents + * + * The file working copy will listen to these changes and may mark + * the working copy as dirty whenever this event fires. + * + * Note: ONLY report changes to the model but not the underlying + * file. The file working copy is tracking changes to the file + * automatically. + */ + readonly onDidChangeContent: Event; + + /** + * An event emitted right before disposing the model. + */ + readonly onWillDispose: Event; + + /** + * Snapshots the model's current content for writing. This must include + * any changes that were made to the model that are in memory. + * + * @param token support for cancellation + */ + snapshot(token: CancellationToken): Promise; + + /** + * Updates the model with the provided contents. The implementation should + * behave in a similar fashion as `IBaseFileWorkingCopyModelFactory#createModel` + * except that here the model already exists and just needs to update to + * the provided contents. + * + * Note: it is expected that the model fires a `onDidChangeContent` event + * as part of the update. + * + * @param the contents to use for the model + * @param token support for cancellation + */ + update(contents: VSBufferReadableStream, token: CancellationToken): Promise; +} + +export interface IBaseFileWorkingCopy extends IWorkingCopy, IDisposable { + + /** + * An event for when the file working copy has been reverted. + */ + readonly onDidRevert: Event; + + /** + * An event for when the file working copy has been disposed. + */ + readonly onWillDispose: Event; + + /** + * Provides access to the underlying model of this file + * based working copy. As long as the file working copy + * has not been resolved, the model is `undefined`. + */ + readonly model: T | undefined; + + /** + * Resolves the file working copy and thus makes the `model` + * available. + */ + resolve(): Promise; +} diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index ba40ccd1df1..5a7e140a6d8 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -7,7 +7,6 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { ETAG_DISABLED, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent, IWriteFileOptions } from 'vs/platform/files/common/files'; import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -28,23 +27,12 @@ import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/com import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; import { IResourceWorkingCopy, ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; +import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel, IBaseFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; -export interface IFileWorkingCopyModelFactory { - - /** - * Asks the file working copy delegate to create a model from the given - * content under the provided resource. The content may originate from - * different sources depending on context: - * - from a backup if that exists - * - from the underlying file resource - * - passed in from the caller - * - * @param resource the `URI` of the model - * @param contents the content of the model to create it - * @param token support for cancellation - */ - createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise; -} +/** + * File specific working copy model factory. + */ +export interface IFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } /** * The underlying model of a file working copy provides some @@ -52,30 +40,10 @@ export interface IFileWorkingCopyModelFactory { * typically only available after the working copy has been * resolved via it's `resolve()` method. */ -export interface IFileWorkingCopyModel extends IDisposable { +export interface IFileWorkingCopyModel extends IBaseFileWorkingCopyModel { - /** - * This event signals ANY changes to the contents of the file - * working copy model, for example: - * - through the user typing into the editor - * - from API usage (e.g. bulk edits) - * - when `IFileWorkingCopyModel#update` is invoked with contents - * that are different from the current contents - * - * The file working copy will listen to these changes and mark - * the working copy as dirty whenever this event fires. - * - * Note: ONLY report changes to the model but not the underlying - * file. The file working copy is tracking changes to the file - * automatically. - */ readonly onDidChangeContent: Event; - /** - * An event emitted right before disposing the model. - */ - readonly onWillDispose: Event; - /** * A version ID of the model. If a `onDidChangeContent` is fired * from the model and the last known saved `versionId` matches @@ -91,28 +59,6 @@ export interface IFileWorkingCopyModel extends IDisposable { */ readonly versionId: unknown; - /** - * Snapshots the model's current content for writing. This must include - * any changes that were made to the model that are in memory. - * - * @param token support for cancellation - */ - snapshot(token: CancellationToken): Promise; - - /** - * Updates the model with the provided contents. The implementation should - * behave in a similar fashion as `IFileWorkingCopyModelFactory#createModel` - * except that here the model already exists and just needs to update to - * the provided contents. - * - * Note: it is expected that the model fires a `onDidChangeContent` event - * as part of the update. - * - * @param the contents to use for the model - * @param token support for cancellation - */ - update(contents: VSBufferReadableStream, token: CancellationToken): Promise; - /** * Close the current undo-redo element. This offers a way * to create an undo/redo stop point. @@ -143,7 +89,7 @@ export interface IFileWorkingCopyModelContentChangedEvent { * of functionality can be built on top, such as saving in * a secure way to prevent data loss. */ -export interface IFileWorkingCopy extends IResourceWorkingCopy { +export interface IFileWorkingCopy extends IResourceWorkingCopy, IBaseFileWorkingCopy { /** * An event for when a file working copy was resolved. @@ -160,23 +106,11 @@ export interface IFileWorkingCopy extends IReso */ readonly onDidSaveError: Event; - /** - * An event for when the file working copy was reverted. - */ - readonly onDidRevert: Event; - /** * An event for when the readonly state of the file working copy changes. */ readonly onDidChangeReadonly: Event; - /** - * Provides access to the underlying model of this file - * based working copy. As long as the file working copy - * has not been resolved, the model is `undefined`. - */ - readonly model: T | undefined; - /** * Resolves a file working copy. */ @@ -217,7 +151,7 @@ export interface IFileWorkingCopy extends IReso export interface IResolvedFileWorkingCopy extends IFileWorkingCopy { /** - * A resolved file working copy has a resolved model `T`. + * A resolved file working copy has a resolved model. */ readonly model: T; } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 0d59f0fa23d..77a33c99997 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -5,9 +5,9 @@ import { Event, Emitter } from 'vs/base/common/event'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; -import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel, IBaseFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -18,13 +18,23 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { emptyStream } from 'vs/base/common/stream'; -export interface IUntitledFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } +/** + * Untitled file specific working copy model factory. + */ +export interface IUntitledFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } + +/** + * The underlying model of a untitled file working copy provides + * some methods for the untitled file working copy to function. + * The model is typically only available after the working copy + * has been resolved via it's `resolve()` method. + */ +export interface IUntitledFileWorkingCopyModel extends IBaseFileWorkingCopyModel { -export interface IUntitledFileWorkingCopyModel extends IFileWorkingCopyModel { readonly onDidChangeContent: Event; } -export interface IUntitledFileWorkingCopyModelContentChangedEvent extends IFileWorkingCopyModelContentChangedEvent { +export interface IUntitledFileWorkingCopyModelContentChangedEvent { /** * Flag that indicates that the content change @@ -35,40 +45,18 @@ export interface IUntitledFileWorkingCopyModelContentChangedEvent extends IFileW readonly isEmpty: boolean; } -export interface IUntitledFileWorkingCopy extends IWorkingCopy, IDisposable { - - /** - * Emits an event when this untitled model is reverted. - */ - readonly onDidRevert: Event; - - /** - * An event for when the file working copy has been disposed. - */ - readonly onWillDispose: Event; - - /** - * Provides access to the underlying model of this untitled - * file based working copy. As long as the untitled file working - * copy has not been resolved, the model is `undefined`. - */ - readonly model: T | undefined; +export interface IUntitledFileWorkingCopy extends IBaseFileWorkingCopy { /** * Whether this untitled file working copy model has an associated file path. */ readonly hasAssociatedFilePath: boolean; - - /** - * Resolves an untitled file working copy. - */ - resolve(): Promise; } export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { /** - * A resolved untitled file working copy has a resolved model `T`. + * A resolved untitled file working copy has a resolved model. */ readonly model: T; } diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index 8c7da14566a..d4ed35dd986 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -61,7 +61,7 @@ export class TestUntitledFileWorkingCopyModel extends Disposable implements IUnt this.versionId++; - this._onDidChangeContent.fire({ isRedoing: false, isUndoing: false, isEmpty: newContents.length === 0 }); + this._onDidChangeContent.fire({ isEmpty: newContents.length === 0 }); } versionId = 0; @@ -169,7 +169,7 @@ suite('UntitledFileWorkingCopy', () => { workingCopy.model?.updateContents('hello dirty'); assert.strictEqual(workingCopy.isDirty(), true); - workingCopy.model?.fireContentChangeEvent({ isEmpty: true, isUndoing: false, isRedoing: false }); + workingCopy.model?.fireContentChangeEvent({ isEmpty: true }); assert.strictEqual(workingCopy.isDirty(), false); }); @@ -183,7 +183,7 @@ suite('UntitledFileWorkingCopy', () => { workingCopy.model?.updateContents('hello dirty'); assert.strictEqual(workingCopy.isDirty(), true); - workingCopy.model?.fireContentChangeEvent({ isEmpty: true, isUndoing: false, isRedoing: false }); + workingCopy.model?.fireContentChangeEvent({ isEmpty: true }); assert.strictEqual(workingCopy.isDirty(), true); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 417fad0cfbe..1dcc6163294 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -75,7 +75,7 @@ suite('UntitledFileWorkingCopyManager', () => { assert.strictEqual(workingCopy1.isDirty(), false); assert.strictEqual(dirtyCounter, 2); - workingCopy2.model?.fireContentChangeEvent({ isEmpty: false, isRedoing: false, isUndoing: false }); + workingCopy2.model?.fireContentChangeEvent({ isEmpty: false }); assert.strictEqual(workingCopy2.isDirty(), true); assert.strictEqual(dirtyCounter, 3); From 2f2f8d7b0f497fc158be06517a6f5be8b97402a9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 08:30:01 +0200 Subject: [PATCH 019/270] untitled file working copy - extract common super type for manager --- .../common/abstractFileWorkingCopyManager.ts | 133 +++++++++++++ .../common/fileWorkingCopyManager.ts | 109 ++--------- .../common/untitledFileWorkingCopyManager.ts | 178 ++++-------------- 3 files changed, 187 insertions(+), 233 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts new file mode 100644 index 00000000000..42773b88bf1 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { Promises } from 'vs/base/common/async'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; + +export interface IBaseFileWorkingCopyManager> extends IDisposable { + + /** + * Access to all known file working copies within the manager. + */ + readonly workingCopies: readonly W[]; + + /** + * Returns the file working copy for the provided resource + * or `undefined` if none. + */ + get(resource: URI): W | undefined; + + /** + * Disposes all working copies of the manager and disposes the manager. This + * method is different from `dispose` in that it will unregister any working + * copy from the `IWorkingCopyService`. Since this impact things like backups, + * the method is `async` because it needs to trigger `save` for any dirty + * working copy to preserve the data. + * + * Callers should make sure to e.g. close any editors associated with the + * working copy. + */ + destroy(): Promise; +} + +export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { + + private readonly mapResourceToWorkingCopy = new ResourceMap(); + + constructor( + @IFileService protected readonly fileService: IFileService, + @ILogService protected readonly logService: ILogService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService + ) { + super(); + } + + protected has(resource: URI): boolean { + return this.mapResourceToWorkingCopy.has(resource); + } + + protected add(resource: URI, workingCopy: W): void { + this.mapResourceToWorkingCopy.set(resource, workingCopy); + } + + protected remove(resource: URI): void { + this.mapResourceToWorkingCopy.delete(resource); + } + + //#region Get / Get all + + get workingCopies(): W[] { + return [...this.mapResourceToWorkingCopy.values()]; + } + + get(resource: URI): W | undefined { + return this.mapResourceToWorkingCopy.get(resource); + } + + //#endregion + + //#region Lifecycle + + override dispose(): void { + super.dispose(); + + // Clear working copy caches + // + // Note: we are not explicitly disposing the working copies + // known to the manager because this can have unwanted side + // effects such as backups getting discarded once the working + // copy unregisters. We have an explicit `destroy` + // for that purpose (https://github.com/microsoft/vscode/pull/123555) + // + this.mapResourceToWorkingCopy.clear(); + } + + async destroy(): Promise { + + // Make sure all dirty working copies are saved to disk + try { + await Promises.settled(this.workingCopies.map(async workingCopy => { + if (workingCopy.isDirty()) { + await this.saveWithFallback(workingCopy); + } + })); + } catch (error) { + this.logService.error(error); + } + + // Dispose all working copies + dispose(this.mapResourceToWorkingCopy.values()); + + // Finally dispose manager + this.dispose(); + } + + private async saveWithFallback(workingCopy: W): Promise { + + // First try regular save + let saveFailed = false; + try { + await workingCopy.save(); + } catch (error) { + saveFailed = true; + } + + // Then fallback to backup if that exists + if (saveFailed || workingCopy.isDirty()) { + const backup = await this.workingCopyBackupService.resolve(workingCopy); + if (backup) { + await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); + } + } + } + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 72a7e3f3c5e..c1bc7d08104 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopySaveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { SaveReason } from 'vs/workbench/common/editor'; @@ -22,13 +22,14 @@ import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/serv import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; /** * The only one that should be dealing with `IFileWorkingCopy` and handle all * operations that are working copy related, such as save/revert, backup * and resolving. */ -export interface IFileWorkingCopyManager extends IDisposable { +export interface IFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { /** * An event for when a file working copy was created. @@ -60,17 +61,6 @@ export interface IFileWorkingCopyManager extend */ readonly onDidRevert: Event>; - /** - * Access to all known file working copies within the manager. - */ - readonly workingCopies: readonly IFileWorkingCopy[]; - - /** - * Returns the file working copy for the provided resource - * or `undefined` if none. - */ - get(resource: URI): IFileWorkingCopy | undefined; - /** * Allows to resolve a file working copy. If the manager already knows * about a file working copy with the same `URI`, it will return that @@ -119,18 +109,6 @@ export interface IFileWorkingCopyManager extend * it is dirty. Once the promise is settled, it is safe to dispose. */ canDispose(workingCopy: IFileWorkingCopy): true | Promise; - - /** - * Disposes all working copies of the manager and disposes the manager. This - * method is different from `dispose` in that it will unregister any working - * copy from the `IWorkingCopyService`. Since this impact things like backups, - * the method is `async` because it needs to trigger `save` for any dirty - * working copy to preserve the data. - * - * Callers should make sure to e.g. close any editors associated with the - * working copy. - */ - destroy(): Promise; } export interface IFileWorkingCopySaveEvent { @@ -180,7 +158,7 @@ export interface IFileWorkingCopySaveAsOptions extends IFileWorkingCopySaveOptio suggestedTarget?: URI; } -export class FileWorkingCopyManager extends Disposable implements IFileWorkingCopyManager { +export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events @@ -204,7 +182,6 @@ export class FileWorkingCopyManager extends Dis //#endregion - private readonly mapResourceToWorkingCopy = new ResourceMap>(); private readonly mapResourceToWorkingCopyListeners = new ResourceMap(); private readonly mapResourceToDisposeListener = new ResourceMap(); private readonly mapResourceToPendingWorkingCopyResolve = new ResourceMap>(); @@ -214,17 +191,17 @@ export class FileWorkingCopyManager extends Dis constructor( private readonly workingCopyTypeId: string, private readonly modelFactory: IFileWorkingCopyModelFactory, - @IFileService private readonly fileService: IFileService, + @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ILogService private readonly logService: ILogService, + @ILogService logService: ILogService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { - super(); + super(fileService, logService, workingCopyBackupService); this.registerListeners(); } @@ -411,18 +388,6 @@ export class FileWorkingCopyManager extends Dis //#endregion - //#region Get / Get all - - get workingCopies(): IFileWorkingCopy[] { - return [...this.mapResourceToWorkingCopy.values()]; - } - - get(resource: URI): IFileWorkingCopy | undefined { - return this.mapResourceToWorkingCopy.get(resource); - } - - //#endregion - //#region Resolve async resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise> { @@ -554,8 +519,8 @@ export class FileWorkingCopyManager extends Dis this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); } - private add(resource: URI, workingCopy: IFileWorkingCopy): void { - const knownWorkingCopy = this.mapResourceToWorkingCopy.get(resource); + protected override add(resource: URI, workingCopy: IFileWorkingCopy): void { + const knownWorkingCopy = this.get(resource); if (knownWorkingCopy === workingCopy) { return; // already cached } @@ -567,12 +532,12 @@ export class FileWorkingCopyManager extends Dis } // Store in cache but remove when working copy gets disposed - this.mapResourceToWorkingCopy.set(resource, workingCopy); + super.add(resource, workingCopy); this.mapResourceToDisposeListener.set(resource, workingCopy.onWillDispose(() => this.remove(resource))); } - private remove(resource: URI): void { - this.mapResourceToWorkingCopy.delete(resource); + protected override remove(resource: URI): void { + super.remove(resource); const disposeListener = this.mapResourceToDisposeListener.get(resource); if (disposeListener) { @@ -723,15 +688,6 @@ export class FileWorkingCopyManager extends Dis override dispose(): void { super.dispose(); - // Clear working copy caches - // - // Note: we are not explicitly disposing the working copies - // known to the manager because this can have unwanted side - // effects such as backups getting discarded once the working - // copy unregisters. We have an explicit `destroy` - // for that purpose (https://github.com/microsoft/vscode/pull/123555) - // - this.mapResourceToWorkingCopy.clear(); this.mapResourceToPendingWorkingCopyResolve.clear(); // Dispose the dispose listeners @@ -743,44 +699,5 @@ export class FileWorkingCopyManager extends Dis this.mapResourceToWorkingCopyListeners.clear(); } - async destroy(): Promise { - - // Make sure all dirty working copies are saved to disk - try { - await Promises.settled(this.workingCopies.map(async workingCopy => { - if (workingCopy.isDirty()) { - await this.saveWithFallback(workingCopy); - } - })); - } catch (error) { - this.logService.error(error); - } - - // Dispose all working copies - dispose(this.mapResourceToWorkingCopy.values()); - - // Finally dispose manager - this.dispose(); - } - - private async saveWithFallback(workingCopy: IFileWorkingCopy): Promise { - - // First try regular save - let saveFailed = false; - try { - await workingCopy.save(); - } catch (error) { - saveFailed = true; - } - - // Then fallback to backup if that exists - if (saveFailed || workingCopy.isDirty()) { - const backup = await this.workingCopyBackupService.resolve(workingCopy); - if (backup) { - await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); - } - } - } - //#endregion } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 674cca7295a..3b2e2d4e9d4 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -4,18 +4,47 @@ *--------------------------------------------------------------------------------------------*/ import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { Event, Emitter } from 'vs/base/common/event'; -import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { Promises } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService } from 'vs/platform/files/common/files'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; + +/** + * The only one that should be dealing with `IUntitledFileWorkingCopy` and + * handle all operations that are working copy related, such as save/revert, + * backup and resolving. + */ +export interface IUntitledFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { + + /** + * An event for when a untitled file working copy changed it's dirty state. + */ + readonly onDidChangeDirty: Event>; + + /** + * An event for when a untitled file working copy is about to be disposed. + */ + readonly onWillDispose: Event>; + + /** + * Resolves an untitled file working copy from the provided options. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Resolves an untitled file working copy from the provided options + * unless an existing working copy already exists with that resource. + */ + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; +} export interface INewUntitledFileWorkingCopyOptions { @@ -52,62 +81,9 @@ export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFil untitledResource: URI; } -/** - * The only one that should be dealing with `IUntitledFileWorkingCopy` and - * handle all operations that are working copy related, such as save/revert, - * backup and resolving. - */ -export interface IUntitledFileWorkingCopyManager extends IDisposable { - - /** - * An event for when a untitled file working copy changed it's dirty state. - */ - readonly onDidChangeDirty: Event>; - - /** - * An event for when a untitled file working copy is about to be disposed. - */ - readonly onWillDispose: Event>; - - /** - * Access to all known untitled file working copies within the manager. - */ - readonly workingCopies: readonly IUntitledFileWorkingCopy[]; - - /** - * Returns an existing untitled file working copy if already created before - * or `undefined` otherwise. - */ - get(resource: URI): IUntitledFileWorkingCopy | undefined; - - /** - * Resolves an untitled file working copy from the provided options. - */ - resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; - resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; - - /** - * Resolves an untitled file working copy from the provided options - * unless an existing working copy already exists with that resource. - */ - resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; - - /** - * Disposes all working copies of the manager and disposes the manager. This - * method is different from `dispose` in that it will unregister any working - * copy from the `IWorkingCopyService`. Since this impact things like backups, - * the method is `async` because it needs to trigger `save` for any dirty - * working copy to preserve the data. - * - * Callers should make sure to e.g. close any editors associated with the - * working copy. - */ - destroy(): Promise; -} - type IInternalUntitledFileWorkingCopyOptions = IExistingUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions; -export class UntitledFileWorkingCopyManager extends Disposable implements IUntitledFileWorkingCopyManager { +export class UntitledFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IUntitledFileWorkingCopyManager { //#region Events @@ -119,32 +95,18 @@ export class UntitledFileWorkingCopyManager>(); - constructor( private readonly workingCopyTypeId: string, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, - @ILogService private readonly logService: ILogService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, - @IFileService private readonly fileService: IFileService + @ILogService logService: ILogService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, + @IFileService fileService: IFileService ) { - super(); + super(fileService, logService, workingCopyBackupService); } - //#region Get / Get all - - get workingCopies(): IUntitledFileWorkingCopy[] { - return [...this.mapResourceToWorkingCopy.values()]; - } - - get(resource: URI): IUntitledFileWorkingCopy | undefined { - return this.mapResourceToWorkingCopy.get(resource); - } - - //#endregion - //#region Resolve async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { @@ -159,7 +121,7 @@ export class UntitledFileWorkingCopyManager { // Registry - this.mapResourceToWorkingCopy.delete(workingCopy.resource); + this.remove(workingCopy.resource); // Listeners listeners.dispose(); }); // Add to cache - this.mapResourceToWorkingCopy.set(workingCopy.resource, workingCopy); + this.add(workingCopy.resource, workingCopy); // If the working copy is dirty right from the beginning, // make sure to emit this as an event @@ -250,62 +212,4 @@ export class UntitledFileWorkingCopyManager { - const workingCopies = Array.from(this.mapResourceToWorkingCopy.values()); - - // Make sure all dirty working copies are saved to disk - try { - await Promises.settled(workingCopies.map(async workingCopy => { - if (workingCopy.isDirty()) { - await this.saveWithFallback(workingCopy); - } - })); - } catch (error) { - this.logService.error(error); - } - - // Dispose all working copies - dispose(workingCopies); - - // Finally dispose manager - this.dispose(); - } - - private async saveWithFallback(workingCopy: IUntitledFileWorkingCopy): Promise { - - // First try regular save - let saveFailed = false; - try { - await workingCopy.save(); - } catch (error) { - saveFailed = true; - } - - // Then fallback to backup if that exists - if (saveFailed || workingCopy.isDirty()) { - const backup = await this.workingCopyBackupService.resolve(workingCopy); - if (backup) { - await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); - } - } - - //#endregion - } } From 956347c4edc9b4cf98084d9a061dbbd9348a5de9 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 18 May 2021 09:40:31 +0200 Subject: [PATCH 020/270] better logic for "notebook open/close, notebook ready when cell-document open event is fired", https://github.com/microsoft/vscode/issues/123655 --- .../notebook.document.test.ts | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index 144f1a0da12..cf4e029e497 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -91,16 +91,27 @@ suite('Notebook Document', function () { test('notebook open/close, notebook ready when cell-document open event is fired', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); let didHappen = false; - const p = utils.asPromise(vscode.workspace.onDidOpenTextDocument).then(doc => { - if (doc.uri.scheme !== 'vscode-notebook-cell') { - return; - } - const notebook = vscode.notebook.notebookDocuments.find(notebook => { - const cell = notebook.getCells().find(cell => cell.document === doc); - return Boolean(cell); + + const p = new Promise((resolve, reject) => { + const sub = vscode.workspace.onDidOpenTextDocument(doc => { + if (doc.uri.scheme !== 'vscode-notebook-cell') { + // ignore other open events + return; + } + const notebook = vscode.notebook.notebookDocuments.find(notebook => { + const cell = notebook.getCells().find(cell => cell.document === doc); + return Boolean(cell); + }); + assert.ok(notebook, `notebook for cell ${doc.uri} NOT found`); + didHappen = true; + sub.dispose(); + resolve(); }); - assert.ok(notebook, `notebook for cell ${doc.uri} NOT found`); - didHappen = true; + + setTimeout(() => { + sub.dispose(); + reject(new Error('TIMEOUT')); + }, 15000); }); await vscode.notebook.openNotebookDocument(uri); From fe2761a026ed585dcab735bffc8590d47f2da267 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 10:22:03 +0200 Subject: [PATCH 021/270] untitled file working copy - add workingcopyservice#get --- .../services/textfile/browser/textFileService.ts | 2 +- .../services/workingCopy/common/workingCopyService.ts | 10 ++++++++++ .../workingCopy/test/common/workingCopyService.test.ts | 2 ++ src/vs/workbench/test/browser/workbenchTestServices.ts | 3 +++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 754fddd0d28..11c67c66e0b 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -457,7 +457,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // Otherwise try to suggest a path that can be saved let suggestedFilename: string | undefined = undefined; if (resource.scheme === Schemas.untitled) { - const model = this.untitledTextEditorService.get(resource); + const model = this.untitled.get(resource); if (model) { // Untitled with associated file path diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index c39b9872644..39a2fa4797e 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -97,6 +97,12 @@ export interface IWorkingCopyService { has(identifier: IWorkingCopyIdentifier): boolean; has(resource: URI): boolean; + /** + * Returns a working copy with the given identifier or `undefined` + * if no such working copy exists. + */ + get(identifier: IWorkingCopyIdentifier): IWorkingCopy | undefined; + //#endregion } @@ -192,6 +198,10 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic return this.mapResourceToWorkingCopies.get(resourceOrIdentifier.resource)?.has(resourceOrIdentifier.typeId) ?? false; } + get(identifier: IWorkingCopyIdentifier): IWorkingCopy | undefined { + return this.mapResourceToWorkingCopies.get(identifier.resource)?.get(identifier.typeId); + } + //#endregion diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index e06a9ba987e..01896acf907 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -35,6 +35,7 @@ suite('WorkingCopyService', () => { const resource1 = URI.file('/some/folder/file.txt'); assert.strictEqual(service.has(resource1), false); assert.strictEqual(service.has({ resource: resource1, typeId: 'testWorkingCopyType' }), false); + assert.strictEqual(service.get({ resource: resource1, typeId: 'testWorkingCopyType' }), undefined); const copy1 = new TestWorkingCopy(resource1); const unregister1 = service.registerWorkingCopy(copy1); @@ -46,6 +47,7 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.isDirty(resource1), false); assert.strictEqual(service.has(resource1), true); assert.strictEqual(service.has(copy1), true); + assert.strictEqual(service.get(copy1), copy1); assert.strictEqual(service.hasDirty, false); copy1.setDirty(true); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 6a23b7a9bf0..31a0a9caab2 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -261,8 +261,11 @@ export class TestServiceAccessor { @IModelService public modelService: ModelServiceImpl, @IFileService public fileService: TestFileService, @IFileDialogService public fileDialogService: TestFileDialogService, + @IDialogService public dialogService: IDialogService, @IWorkingCopyService public workingCopyService: IWorkingCopyService, @IEditorService public editorService: TestEditorService, + @IWorkbenchEnvironmentService public environmentService: IWorkbenchEnvironmentService, + @IPathService public pathService: IPathService, @IEditorGroupsService public editorGroupService: IEditorGroupsService, @IEditorOverrideService public editorOverrideService: IEditorOverrideService, @IModeService public modeService: IModeService, From b94b2d2cf0195f758c0b6161892e4b5234317b7c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 11:05:26 +0200 Subject: [PATCH 022/270] untitled file working copy - wire in save support --- .../notebook/common/notebookEditorModel.ts | 6 +- .../common/abstractFileWorkingCopy.ts | 13 + .../common/abstractFileWorkingCopyManager.ts | 253 +++++++++++++++++- .../common/fileWorkingCopyManager.ts | 129 ++------- .../common/untitledFileWorkingCopy.ts | 18 +- .../common/untitledFileWorkingCopyManager.ts | 37 ++- .../test/browser/fileWorkingCopy.test.ts | 2 +- .../browser/fileWorkingCopyManager.test.ts | 30 ++- .../browser/untitledFileWorkingCopy.test.ts | 1 + .../untitledFileWorkingCopyManager.test.ts | 17 +- 10 files changed, 373 insertions(+), 133 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 511261f2ada..03cb38b4b63 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -28,7 +28,7 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IFileWorkingCopyManager, IFileWorkingCopySaveAsOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { filter } from 'vs/base/common/objects'; //#region --- complex content provider @@ -491,8 +491,8 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE return this; } - async saveAs(target: URI, options?: IFileWorkingCopySaveAsOptions): Promise { - const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target, options); + async saveAs(target: URI): Promise { + const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target); if (!newWorkingCopy) { return undefined; } diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts index 36b4f2ac181..aa49c394e09 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts @@ -96,4 +96,17 @@ export interface IBaseFileWorkingCopy exten * available. */ resolve(): Promise; + + /** + * Whether we have a resolved model or not. + */ + isResolved(): this is IBaseResolvedFileWorkingCopy; +} + +export interface IBaseResolvedFileWorkingCopy extends IBaseFileWorkingCopy { + + /** + * A resolved file working copy has a resolved model. + */ + readonly model: T; } diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 42773b88bf1..e9021cb612d 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { Promises } from 'vs/base/common/async'; @@ -11,6 +12,19 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { FileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { IConfirmation, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { basename, dirname, isEqual, joinPath, toLocalResource } from 'vs/base/common/resources'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { isValidBasename } from 'vs/base/common/extpath'; +import { ISaveOptions } from 'vs/workbench/common/editor'; export interface IBaseFileWorkingCopyManager> extends IDisposable { @@ -38,14 +52,31 @@ export interface IBaseFileWorkingCopyManager; } +export interface IBaseFileWorkingCopySaveAsOptions extends ISaveOptions { + + /** + * Optional target resource to suggest to the user in case + * no taget resource is provided to save to. + */ + suggestedTarget?: URI; +} + export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { private readonly mapResourceToWorkingCopy = new ResourceMap(); constructor( + protected readonly workingCopyTypeId: string, @IFileService protected readonly fileService: IFileService, @ILogService protected readonly logService: ILogService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, + @IWorkingCopyFileService protected readonly workingCopyFileService: IWorkingCopyFileService, + @IDialogService private readonly dialogService: IDialogService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IPathService private readonly pathService: IPathService ) { super(); } @@ -72,6 +103,226 @@ export abstract class BaseFileWorkingCopyManager { + const workingCopy = this.get(resource); + + // Untitled + if (workingCopy instanceof UntitledFileWorkingCopy) { + let targetUri: URI | undefined; + + // Untitled with associated file path is taken as is + if (workingCopy.hasAssociatedFilePath) { + targetUri = await this.suggestSavePath(resource); + } + + // Otherwise ask user for a target path to save to + else { + targetUri = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(resource), options?.availableFileSystems); + } + + // Save as if target provided + if (targetUri) { + return this.saveAs(resource, targetUri, options); + } + } + + // File + else if (workingCopy) { + const success = await workingCopy.save(options); + if (success) { + return workingCopy; + } + } + + return undefined; + } + + async saveAs(source: URI, target?: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise { + + // Get to target resource + if (!target) { + target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + } + + if (!target) { + return; // user canceled + } + + // Just save if target is same as working copies own resource + if (isEqual(source, target)) { + return this.save(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + } + + // If the target is different but of same identity, we + // move the source to the target, knowing that the + // underlying file system cannot have both and then save. + // However, this will only work if the source exists + // and is not orphaned, so we need to check that too. + if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) { + await this.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); + + // At this point we don't know whether we have a + // working copy for the source or the target URI so we + // simply try to save with both resources. + let targetWorkingCopy = await this.save(source, options); + if (!targetWorkingCopy) { + targetWorkingCopy = await this.save(target, options); + } + + return targetWorkingCopy; + } + + // Do it + return this.doSaveAs(source, target, options); + } + + private async doSaveAs(source: URI, target: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise { + let sourceContents: VSBufferReadableStream; + + // If the source is an existing file working copy, we can directly + // use that to copy the contents to the target destination + const sourceWorkingCopy = this.get(source); + if (sourceWorkingCopy?.isResolved()) { + sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); + } + + // Otherwise we resolve the contents from the underlying file + else { + sourceContents = (await this.fileService.readFileStream(source)).value; + } + + // Save the contents through working copy to benefit from save + // participants and handling a potential already existing target + return this.doSaveAsWorkingCopy(source, sourceWorkingCopy, sourceContents, target, options); + } + + private async doSaveAsWorkingCopy(source: URI, sourceWorkingCopy: W | undefined, sourceContents: VSBufferReadableStream, target: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise { + + // Prefer an existing file working copy if it is already resolved + // for the given target resource + let targetFileExists = false; + let targetFileWorkingCopy = this.getFile(target); + if (targetFileWorkingCopy?.isResolved()) { + targetFileExists = true; + } + + // Otherwise create the target working copy empty if + // it does not exist already and resolve it from there + else { + targetFileExists = await this.fileService.exists(target); + + // Create target file adhoc if it does not exist yet + if (!targetFileExists) { + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); + } + + // At this point we need to resolve the target working copy + // and we have to do an explicit check if the source URI + // equals the target via URI identity. If they match and we + // have had an existing working copy with the source, we + // prefer that one over resolving the target. Otherwiese we + // would potentially introduce a + if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { + targetFileWorkingCopy = await this.doResolve(source); + } else { + targetFileWorkingCopy = await this.doResolve(target); + } + } + + // Confirm to overwrite if we have an untitled file working copy with associated path where + // the file actually exists on disk and we are instructed to save to that file path. + // This can happen if the file was created after the untitled file was opened. + // See https://github.com/microsoft/vscode/issues/67946 + if ( + sourceWorkingCopy instanceof UntitledFileWorkingCopy && + sourceWorkingCopy.hasAssociatedFilePath && + targetFileExists && + this.uriIdentityService.extUri.isEqual(target, toLocalResource(sourceWorkingCopy.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme)) + ) { + const overwrite = await this.confirmOverwrite(target); + if (!overwrite) { + return undefined; + } + } + + // Take over content from source to target + await targetFileWorkingCopy.model?.update(sourceContents, CancellationToken.None); + + // Save target + await targetFileWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + + // Revert the source + await sourceWorkingCopy?.revert(); + + return targetFileWorkingCopy; + } + + protected abstract doResolve(resource: URI): Promise; + + private async confirmOverwrite(resource: URI): Promise { + const confirm: IConfirmation = { + message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), + detail: localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + const result = await this.dialogService.confirm(confirm); + return result.confirmed; + } + + private async suggestSavePath(resource: URI): Promise { + + // Just take the resource as is if the file service can handle it + if (this.fileService.canHandleResource(resource)) { + return resource; + } + + const remoteAuthority = this.environmentService.remoteAuthority; + const workingCopy = this.get(resource); + + // Otherwise try to suggest a path that can be saved + let suggestedFilename: string | undefined = undefined; + if (workingCopy instanceof UntitledFileWorkingCopy) { + + // Untitled with associated file path + if (workingCopy.hasAssociatedFilePath) { + return toLocalResource(resource, remoteAuthority, this.pathService.defaultUriScheme); + } + + // Untitled without associated file path: use name + // of untitled working copy if it is a valid path name + let untitledName = workingCopy.name; + if (!isValidBasename(untitledName)) { + untitledName = basename(resource); + } + + suggestedFilename = untitledName; + } + + // Fallback to basename of resource + if (!suggestedFilename) { + suggestedFilename = basename(resource); + } + + // Try to place where last active file was if any + // Otherwise fallback to user home + return joinPath(await this.fileDialogService.defaultFilePath(), suggestedFilename); + } + //#endregion //#region Lifecycle diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index c1bc7d08104..d1222647321 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -16,13 +16,16 @@ import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { joinPath } from 'vs/base/common/resources'; import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager, IBaseFileWorkingCopySaveAsOptions } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; /** * The only one that should be dealing with `IFileWorkingCopy` and handle all @@ -101,7 +104,7 @@ export interface IFileWorkingCopyManager extend * cancellation */ saveAs(source: URI, target: URI, options?: IFileWorkingCopySaveOptions): Promise | undefined>; - saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined>; /** * Waits for the file working copy to be ready to be disposed. There may be @@ -149,15 +152,6 @@ export interface IFileWorkingCopyResolveOptions { }; } -export interface IFileWorkingCopySaveAsOptions extends IFileWorkingCopySaveOptions { - - /** - * Optional target resource to suggest to the user in case - * no taget resource is provided to save to. - */ - suggestedTarget?: URI; -} - export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events @@ -189,19 +183,23 @@ export class FileWorkingCopyManager extends Bas private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); constructor( - private readonly workingCopyTypeId: string, + workingCopyTypeId: string, private readonly modelFactory: IFileWorkingCopyModelFactory, @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, - @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, + @IFileDialogService fileDialogService: IFileDialogService, + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IDialogService dialogService: IDialogService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IPathService pathService: IPathService ) { - super(fileService, logService, workingCopyBackupService); + super(workingCopyTypeId, fileService, logService, workingCopyBackupService, fileDialogService, uriIdentityService, workingCopyFileService, dialogService, workingCopyService, environmentService, pathService); this.registerListeners(); } @@ -496,6 +494,10 @@ export class FileWorkingCopyManager extends Bas } } + protected doResolve(resource: URI): Promise> { + return this.resolve(resource); + } + private joinPendingResolve(resource: URI): Promise | undefined { const pendingWorkingCopyResolve = this.mapResourceToPendingWorkingCopyResolve.get(resource); if (pendingWorkingCopyResolve) { @@ -554,99 +556,6 @@ export class FileWorkingCopyManager extends Bas //#endregion - //#region Save As... - - async saveAs(source: URI, target?: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { - - // If not provided, ask user for target - if (!target) { - target = await this.fileDialogService.pickFileToSave(options?.suggestedTarget ?? source); - - if (!target) { - return undefined; // user canceled - } - } - - // Do it - return this.doSaveAs(source, target, options); - } - - private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { - let sourceContents: VSBufferReadableStream; - - // If the source is an existing file working copy, we can directly - // use that to copy the contents to the target destination - const sourceWorkingCopy = this.get(source); - if (sourceWorkingCopy?.isResolved()) { - sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); - } - - // Otherwise we resolve the contents from the underlying file - else { - sourceContents = (await this.fileService.readFileStream(source)).value; - } - - // Save the contents through working copy to benefit from save - // participants and handling a potential already existing target - return this.doSaveAsWorkingCopy(source, sourceContents, target, options); - } - - private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise> { - - // Prefer an existing working copy if it is already resolved - // for the given target resource - let targetExists = false; - let targetWorkingCopy = this.get(target); - if (targetWorkingCopy?.isResolved()) { - targetExists = true; - } - - // Otherwise create the target working copy empty if - // it does not exist already and resolve it from there - else { - targetExists = await this.fileService.exists(target); - - // Create target file adhoc if it does not exist yet - if (!targetExists) { - await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); - } - - // At this point we need to resolve the target working copy - // and we have to do an explicit check if the source URI - // equals the target via URI identity. If they match and we - // have had an existing working copy with the source, we - // prefer that one over resolving the target. Otherwiese we - // would potentially introduce a - if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { - targetWorkingCopy = await this.resolve(source); - } else { - targetWorkingCopy = await this.resolve(target); - } - } - - // Take over content from source to target - await targetWorkingCopy.model?.update(sourceContents, CancellationToken.None); - - // Save target - await targetWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); - - // Revert the source - await this.doRevert(source); - - return targetWorkingCopy; - } - - private async doRevert(resource: URI): Promise { - const workingCopy = this.get(resource); - if (!workingCopy) { - return undefined; - } - - return workingCopy.revert(); - } - - //#endregion - //#region Lifecycle canDispose(workingCopy: IFileWorkingCopy): true | Promise { diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 77a33c99997..21891effe76 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -61,6 +61,14 @@ export interface IResolvedUntitledFileWorkingCopy { + + /** + * A delegate to enable saving of untitled file working copies. + */ + (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; +} + export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.Untitled; @@ -91,6 +99,7 @@ export class UntitledFileWorkingCopy ex readonly hasAssociatedFilePath: boolean, private readonly initialValue: VSBufferReadableStream | undefined, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @ILogService private readonly logService: ILogService @@ -235,14 +244,7 @@ export class UntitledFileWorkingCopy ex async save(options?: ISaveOptions): Promise { this.trace('[untitled file working copy] save() - enter'); - // TODO needs to extract the code for bringing up save dialog - // or use the associated file path as target - // Also, who is disposing the untitled after save and open the - // new editor? - - await this.revert(); - - return true; + return this.saveDelegate(this, options); } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 3b2e2d4e9d4..344c038662f 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -15,6 +15,12 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService } from 'vs/platform/files/common/files'; import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; /** * The only one that should be dealing with `IUntitledFileWorkingCopy` and @@ -81,7 +87,7 @@ export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFil untitledResource: URI; } -type IInternalUntitledFileWorkingCopyOptions = IExistingUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions; +type IInternalUntitledFileWorkingCopyOptions = INewUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions & IExistingUntitledFileWorkingCopyOptions; export class UntitledFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IUntitledFileWorkingCopyManager { @@ -96,19 +102,29 @@ export class UntitledFileWorkingCopyManager, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IFileService fileService: IFileService, @ILabelService private readonly labelService: ILabelService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, + @IFileDialogService fileDialogService: IFileDialogService, + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, - @IFileService fileService: IFileService + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IDialogService dialogService: IDialogService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IPathService pathService: IPathService ) { - super(fileService, logService, workingCopyBackupService); + super(workingCopyTypeId, fileService, logService, workingCopyBackupService, fileDialogService, uriIdentityService, workingCopyFileService, dialogService, workingCopyService, environmentService, pathService); } //#region Resolve + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { const workingCopy = this.doCreateOrGet(options); await workingCopy.resolve(); @@ -116,6 +132,10 @@ export class UntitledFileWorkingCopyManager> { + return this.resolve({ untitledResource: resource }); + } + private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { const massagedOptions = this.massageOptions(options); @@ -176,7 +196,12 @@ export class UntitledFileWorkingCopyManager (async () => { + const result = await this.save(workingCopy.resource, options); + + return result ? true : false; + })() ) as unknown as IUntitledFileWorkingCopy; this.registerWorkingCopy(workingCopy); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index 47ab58bb6a7..81ef604cc9e 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -98,7 +98,7 @@ suite('FileWorkingCopy', function () { let workingCopy: FileWorkingCopy; function createWorkingCopy(uri: URI = resource) { - return new FileWorkingCopy('testWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); + return new FileWorkingCopy('testFileWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); } setup(() => { diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 4e456cc0f32..d730021d380 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -27,7 +27,23 @@ suite('FileWorkingCopyManager', () => { accessor = instantiationService.createInstance(TestServiceAccessor); const factory = new TestFileWorkingCopyModelFactory(); - manager = new FileWorkingCopyManager('testWorkingCopyType', factory, accessor.fileService, accessor.lifecycleService, accessor.labelService, instantiationService, accessor.logService, accessor.fileDialogService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService); + manager = new FileWorkingCopyManager( + 'testFileWorkingCopyType', + factory, + accessor.fileService, + accessor.lifecycleService, + accessor.labelService, + instantiationService, + accessor.logService, + accessor.fileDialogService, + accessor.workingCopyFileService, + accessor.workingCopyBackupService, + accessor.uriIdentityService, + accessor.dialogService, + accessor.workingCopyService, + accessor.environmentService, + accessor.pathService + ); }); teardown(() => { @@ -49,7 +65,7 @@ suite('FileWorkingCopyManager', () => { const workingCopy1 = await resolvePromise; assert.ok(workingCopy1); assert.ok(workingCopy1.model); - assert.strictEqual(workingCopy1.typeId, 'testWorkingCopyType'); + assert.strictEqual(workingCopy1.typeId, 'testFileWorkingCopyType'); assert.strictEqual(manager.get(resource), workingCopy1); const workingCopy2 = await manager.resolve(resource); @@ -527,7 +543,15 @@ suite('FileWorkingCopyManager', () => { // the same in that case assert.strictEqual(source.toString(), result?.resource.toString()); } else { - assert.strictEqual(target.toString(), result?.resource.toString()); + if (resolveSource || resolveTarget) { + assert.strictEqual(target.toString(), result?.resource.toString()); + } else { + if (accessor.uriIdentityService.extUri.isEqual(source, target)) { + assert.strictEqual(undefined, result); + } else { + assert.strictEqual(target.toString(), result?.resource.toString()); + } + } } if (resolveSource) { diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index d4ed35dd986..39596dcfffa 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -103,6 +103,7 @@ suite('UntitledFileWorkingCopy', () => { hasAssociatedFilePath, initialValue.length > 0 ? bufferToStream(VSBuffer.fromString(initialValue)) : undefined, factory, + async (workingCopy, options) => { return true; }, accessor.workingCopyService, accessor.workingCopyBackupService, accessor.logService diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 1dcc6163294..424426b8760 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -25,7 +25,22 @@ suite('UntitledFileWorkingCopyManager', () => { accessor = instantiationService.createInstance(TestServiceAccessor); const factory = new TestUntitledFileWorkingCopyModelFactory(); - manager = new UntitledFileWorkingCopyManager('testUntitledWorkingCopyType', factory, instantiationService, accessor.labelService, accessor.logService, accessor.workingCopyBackupService, accessor.fileService); + manager = new UntitledFileWorkingCopyManager( + 'testUntitledWorkingCopyType', + factory, + accessor.fileService, + accessor.labelService, + instantiationService, + accessor.logService, + accessor.fileDialogService, + accessor.workingCopyFileService, + accessor.workingCopyBackupService, + accessor.uriIdentityService, + accessor.dialogService, + accessor.workingCopyService, + accessor.environmentService, + accessor.pathService + ); }); teardown(() => { From b2c329807654d4a839c59d0f7ecf0a67c6b907bb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 11:11:23 +0200 Subject: [PATCH 023/270] untitled file working copy - fix tests --- .../services/workingCopy/common/untitledFileWorkingCopy.ts | 2 +- .../workingCopy/test/browser/untitledFileWorkingCopy.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 21891effe76..ef9b390531f 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -241,7 +241,7 @@ export class UntitledFileWorkingCopy ex //#region Save - async save(options?: ISaveOptions): Promise { + save(options?: ISaveOptions): Promise { this.trace('[untitled file working copy] save() - enter'); return this.saveDelegate(this, options); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index 39596dcfffa..16219cd5a69 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -103,7 +103,7 @@ suite('UntitledFileWorkingCopy', () => { hasAssociatedFilePath, initialValue.length > 0 ? bufferToStream(VSBuffer.fromString(initialValue)) : undefined, factory, - async (workingCopy, options) => { return true; }, + async workingCopy => { await workingCopy.revert(); return true; }, accessor.workingCopyService, accessor.workingCopyBackupService, accessor.logService From 3f6e29238c52521282bac9974703547bb61be20c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 18 May 2021 05:07:43 -0700 Subject: [PATCH 024/270] Show status icon in tooltip Fixes #124063 --- src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index c033e468a47..34b1151d9f3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -248,7 +248,7 @@ class TerminalTabsRenderer implements IListRenderer Date: Tue, 18 May 2021 14:10:27 +0200 Subject: [PATCH 025/270] remove todos about ipynb cell ids --- src/vs/vscode.proposed.d.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 64675d0840f..bb436dbfdaf 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1043,7 +1043,6 @@ declare module 'vscode' { * * NotebookCell instances are immutable and are kept in sync for as long as they are part of their notebook. */ - // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md export interface NotebookCell { /** @@ -1308,7 +1307,6 @@ declare module 'vscode' { /** * NotebookCellData is the raw representation of notebook cells. Its is part of {@link NotebookData `NotebookData`}. */ - // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md export class NotebookCellData { /** From b0272010af8392d13ec76b88ef56dc5eb05f06ae Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 18 May 2021 05:30:13 -0700 Subject: [PATCH 026/270] Disable bell sound completely It was making a sound only on start up Fixes #123813 --- src/vs/workbench/contrib/terminal/browser/terminalInstance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 9d47112572d..2ea850cfacc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -496,7 +496,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { letterSpacing: font.letterSpacing, lineHeight: font.lineHeight, minimumContrastRatio: config.minimumContrastRatio, - bellStyle: config.enableBell ? 'sound' : 'none', + bellStyle: 'none', macOptionIsMeta: config.macOptionIsMeta, macOptionClickForcesSelection: config.macOptionClickForcesSelection, rightClickSelectsWord: config.rightClickBehavior === 'selectWord', From 1c6e481373e972fa14629977366389d555a6e339 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 14:52:15 +0200 Subject: [PATCH 027/270] untitled file working copy - some code :lipstick: --- .../common/abstractFileWorkingCopyManager.ts | 151 +++++++++--------- .../common/fileWorkingCopyManager.ts | 27 +--- .../common/untitledFileWorkingCopy.ts | 7 +- 3 files changed, 86 insertions(+), 99 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index e9021cb612d..f2f143e813e 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -39,6 +39,29 @@ export interface IBaseFileWorkingCopyManager; + saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise; + /** * Disposes all working copies of the manager and disposes the manager. This * method is different from `dispose` in that it will unregister any working @@ -119,7 +142,7 @@ export abstract class BaseFileWorkingCopyManager { const workingCopy = this.get(resource); - // Untitled + // Untitled: is always a "Save As" if (workingCopy instanceof UntitledFileWorkingCopy) { let targetUri: URI | undefined; @@ -139,7 +162,7 @@ export abstract class BaseFileWorkingCopyManager { - - // Prefer an existing file working copy if it is already resolved - // for the given target resource - let targetFileExists = false; - let targetFileWorkingCopy = this.getFile(target); - if (targetFileWorkingCopy?.isResolved()) { - targetFileExists = true; - } - - // Otherwise create the target working copy empty if - // it does not exist already and resolve it from there - else { - targetFileExists = await this.fileService.exists(target); - - // Create target file adhoc if it does not exist yet - if (!targetFileExists) { - await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); - } - - // At this point we need to resolve the target working copy - // and we have to do an explicit check if the source URI - // equals the target via URI identity. If they match and we - // have had an existing working copy with the source, we - // prefer that one over resolving the target. Otherwiese we - // would potentially introduce a - if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { - targetFileWorkingCopy = await this.doResolve(source); - } else { - targetFileWorkingCopy = await this.doResolve(target); - } - } + // Resolve target + const { targetFileExists, targetFileWorkingCopy } = await this.doResolveSaveTarget(source, target); // Confirm to overwrite if we have an untitled file working copy with associated path where // the file actually exists on disk and we are instructed to save to that file path. @@ -270,6 +256,42 @@ export abstract class BaseFileWorkingCopyManager { + + // Prefer an existing file working copy if it is already resolved + // for the given target resource + let targetFileExists = false; + let targetFileWorkingCopy = this.getFile(target); + if (targetFileWorkingCopy?.isResolved()) { + targetFileExists = true; + } + + // Otherwise create the target working copy empty if + // it does not exist already and resolve it from there + else { + targetFileExists = await this.fileService.exists(target); + + // Create target file adhoc if it does not exist yet + if (!targetFileExists) { + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); + } + + // At this point we need to resolve the target working copy + // and we have to do an explicit check if the source URI + // equals the target via URI identity. If they match and we + // have had an existing working copy with the source, we + // prefer that one over resolving the target. Otherwise we + // would potentially introduce a + if (this.uriIdentityService.extUri.isEqual(source, target) && this.has(source)) { + targetFileWorkingCopy = await this.doResolve(source); + } else { + targetFileWorkingCopy = await this.doResolve(target); + } + } + + return { targetFileExists, targetFileWorkingCopy }; + } + protected abstract doResolve(resource: URI): Promise; private async confirmOverwrite(resource: URI): Promise { @@ -286,41 +308,24 @@ export abstract class BaseFileWorkingCopyManager { - // Just take the resource as is if the file service can handle it + // 1.) Just take the resource as is if the file service can handle it if (this.fileService.canHandleResource(resource)) { return resource; } - const remoteAuthority = this.environmentService.remoteAuthority; + // 2.) Pick the associated file path for untitled working copies if any const workingCopy = this.get(resource); - - // Otherwise try to suggest a path that can be saved - let suggestedFilename: string | undefined = undefined; - if (workingCopy instanceof UntitledFileWorkingCopy) { - - // Untitled with associated file path - if (workingCopy.hasAssociatedFilePath) { - return toLocalResource(resource, remoteAuthority, this.pathService.defaultUriScheme); - } - - // Untitled without associated file path: use name - // of untitled working copy if it is a valid path name - let untitledName = workingCopy.name; - if (!isValidBasename(untitledName)) { - untitledName = basename(resource); - } - - suggestedFilename = untitledName; + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + return toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); } - // Fallback to basename of resource - if (!suggestedFilename) { - suggestedFilename = basename(resource); + // 3.) Pick the working copy name if valid joined with default path + if (workingCopy && isValidBasename(workingCopy.name)) { + return joinPath(await this.fileDialogService.defaultFilePath(), workingCopy.name); } - // Try to place where last active file was if any - // Otherwise fallback to user home - return joinPath(await this.fileDialogService.defaultFilePath(), suggestedFilename); + // 4.) Finally fallback to the name of the resource joined with default path + return joinPath(await this.fileDialogService.defaultFilePath(), basename(resource)); } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index d1222647321..80e427d8867 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -5,7 +5,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopySaveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { SaveReason } from 'vs/workbench/common/editor'; import { ResourceMap } from 'vs/base/common/map'; import { Promises, ResourceQueue } from 'vs/base/common/async'; @@ -22,7 +22,7 @@ import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/serv import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager, IBaseFileWorkingCopySaveAsOptions } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -83,29 +83,6 @@ export interface IFileWorkingCopyManager extend */ resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; - /** - * Implements "Save As" for file based working copies. The API is `URI` based - * because it works even without resolved file working copies. If a file working - * copy exists for any given `URI`, the implementation will deal with them properly - * (e.g. dirty contents of the source will be written to the target and the source - * will be reverted). - * - * Note: it is possible that the returned file working copy has a different `URI` - * than the `target` that was passed in. Based on URI identity, the file working - * copy may chose to return an existing file working copy with different casing - * to respect file systems that are case insensitive. - * - * Note: Callers must `dispose` the working copy when no longer needed. - * - * @param source the source resource to save as - * @param target the optional target resource to save to. if not defined, the user - * will be asked for input - * @returns the target working copy that was saved to or `undefined` in case of - * cancellation - */ - saveAs(source: URI, target: URI, options?: IFileWorkingCopySaveOptions): Promise | undefined>; - saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined>; - /** * Waits for the file working copy to be ready to be disposed. There may be * conditions under which the file working copy cannot be disposed, e.g. when diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index ef9b390531f..536517a7431 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -51,6 +51,11 @@ export interface IUntitledFileWorkingCopy; } export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { @@ -242,7 +247,7 @@ export class UntitledFileWorkingCopy ex //#region Save save(options?: ISaveOptions): Promise { - this.trace('[untitled file working copy] save() - enter'); + this.trace('[untitled file working copy] save()'); return this.saveDelegate(this, options); } From 414e5dbf1f870bc527ebc587cbbb5f6eee9bfba6 Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 18 May 2021 15:05:11 +0200 Subject: [PATCH 028/270] install additional debuggers only when at least one debugger present --- src/vs/workbench/contrib/debug/browser/debug.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index befa9ebfdae..634f016ea68 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -306,6 +306,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { id: 'debug.installAdditionalDebuggers', title: nls.localize({ key: 'miInstallAdditionalDebuggers', comment: ['&& denotes a mnemonic'] }, "&&Install Additional Debuggers...") }, + when: CONTEXT_DEBUGGERS_AVAILABLE, order: 1 }); From 0f5ceb2f1db6b67a4fe3552a47e3075192977ed5 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 18 May 2021 15:06:45 +0200 Subject: [PATCH 029/270] Add context menu for tunnel protocol Fixes #123750 --- src/vs/platform/actions/common/actions.ts | 1 + .../contrib/remote/browser/tunnelView.ts | 73 ++++++++++++++++++- .../remote/common/remoteExplorerService.ts | 32 +++++++- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index abe86723b10..d073dea5950 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -129,6 +129,7 @@ export class MenuId { static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TunnelContext = new MenuId('TunnelContext'); + static readonly TunnelProtocol = new MenuId('TunnelProtocol'); static readonly TunnelPortInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly TunnelLocalAddressInline = new MenuId('TunnelLocalAddressInline'); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index adb05e76587..24ebccc190e 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -23,7 +23,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IMenuService, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy, TunnelEditId, mapHasAddressLocalhostOrAllInterfaces } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy, TunnelEditId, mapHasAddressLocalhostOrAllInterfaces, TunnelProtocol, Attributes } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -65,6 +65,13 @@ class TunnelTreeVirtualDelegate implements ITableVirtualDelegate { } } +function toTunnelProtocol(value: string | undefined): TunnelProtocol { + if (value === TunnelProtocol.Https) { + return TunnelProtocol.Https; + } + return TunnelProtocol.Http; +} + export interface ITunnelViewModel { readonly onForwardedPortsChanged: Event; readonly all: TunnelItem[]; @@ -422,7 +429,8 @@ class ActionBarRenderer extends Disposable implements ITableRenderer('tunnelType', TunnelType.Add, true); export const TunnelCloseableContextKey = new RawContextKey('tunnelCloseable', false, true); const TunnelPrivacyContextKey = new RawContextKey('tunnelPrivacy', undefined, true); +const TunnelProtocolContextKey = new RawContextKey('tunnelProtocol', TunnelProtocol.Http, true); const TunnelViewFocusContextKey = new RawContextKey('tunnelViewFocus', false, nls.localize('tunnel.focusContext', "Whether the Ports view has focus.")); const TunnelViewSelectionKeyName = 'tunnelViewSelection'; const TunnelViewSelectionContextKey = new RawContextKey(TunnelViewSelectionKeyName, undefined, true); @@ -681,6 +690,7 @@ export class TunnelPanel extends ViewPane { private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; private tunnelPrivacyContext: IContextKey; + private tunnelProtocolContext: IContextKey; private tunnelViewFocusContext: IContextKey; private tunnelViewSelectionContext: IContextKey; private tunnelViewMultiSelectionContext: IContextKey; @@ -714,6 +724,7 @@ export class TunnelPanel extends ViewPane { this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService); this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService); this.tunnelPrivacyContext = TunnelPrivacyContextKey.bindTo(contextKeyService); + this.tunnelProtocolContext = TunnelProtocolContextKey.bindTo(contextKeyService); this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService); this.tunnelViewSelectionContext = TunnelViewSelectionContextKey.bindTo(contextKeyService); this.tunnelViewMultiSelectionContext = TunnelViewMultiSelectionContextKey.bindTo(contextKeyService); @@ -876,12 +887,14 @@ export class TunnelPanel extends ViewPane { this.tunnelTypeContext.set(item.tunnelType); this.tunnelCloseableContext.set(!!item.closeable); this.tunnelPrivacyContext.set(item.privacy); + this.tunnelProtocolContext.set(toTunnelProtocol(item.localUri?.scheme)); this.portChangableContextKey.set(!!item.localPort); } else { this.tunnelTypeContext.reset(); this.tunnelViewSelectionContext.reset(); this.tunnelCloseableContext.reset(); this.tunnelPrivacyContext.reset(); + this.tunnelProtocolContext.reset(); this.portChangableContextKey.reset(); } } @@ -910,11 +923,13 @@ export class TunnelPanel extends ViewPane { this.tunnelTypeContext.set(node.tunnelType); this.tunnelCloseableContext.set(!!node.closeable); this.tunnelPrivacyContext.set(node.privacy); + this.tunnelProtocolContext.set(toTunnelProtocol(node.localUri?.scheme)); this.portChangableContextKey.set(!!node.localPort); } else { this.tunnelTypeContext.set(TunnelType.Add); this.tunnelCloseableContext.set(false); this.tunnelPrivacyContext.set(undefined); + this.tunnelProtocolContext.set(undefined); this.portChangableContextKey.set(false); } @@ -1365,6 +1380,34 @@ namespace MakePortPrivateAction { } } +namespace SetTunnelProtocolAction { + export const ID_HTTP = 'remote.tunnel.setProtocolHttp'; + export const ID_HTTPS = 'remote.tunnel.setProtocolHttps'; + export const LABEL_HTTP = nls.localize('remote.tunnel.protocolHttp', "HTTP"); + export const LABEL_HTTPS = nls.localize('remote.tunnel.protocolHttps', "HTTPS"); + + async function handler(arg: any, protocol: TunnelProtocol, remoteExplorerService: IRemoteExplorerService) { + if (arg instanceof TunnelItem) { + const attributes: Partial = { + protocol + }; + return remoteExplorerService.tunnelModel.configPortsAttributes.addAttributes(arg.remotePort, attributes); + } + } + + export function handlerHttp(): ICommandHandler { + return async (accessor, arg) => { + return handler(arg, TunnelProtocol.Http, accessor.get(IRemoteExplorerService)); + }; + } + + export function handlerHttps(): ICommandHandler { + return async (accessor, arg) => { + return handler(arg, TunnelProtocol.Https, accessor.get(IRemoteExplorerService)); + }; + } +} + const tunnelViewCommandsWeightBonus = 10; // give our commands a little bit more weight over other default list/tree commands const isForwardedExpr = TunnelTypeContextKey.isEqualTo(TunnelType.Forwarded); @@ -1410,6 +1453,8 @@ CommandsRegistry.registerCommand(CopyAddressAction.COMMANDPALETTE_ID, CopyAddres CommandsRegistry.registerCommand(ChangeLocalPortAction.ID, ChangeLocalPortAction.handler()); CommandsRegistry.registerCommand(MakePortPublicAction.ID, MakePortPublicAction.handler()); CommandsRegistry.registerCommand(MakePortPrivateAction.ID, MakePortPrivateAction.handler()); +CommandsRegistry.registerCommand(SetTunnelProtocolAction.ID_HTTP, SetTunnelProtocolAction.handlerHttp()); +CommandsRegistry.registerCommand(SetTunnelProtocolAction.ID_HTTPS, SetTunnelProtocolAction.handlerHttps()); MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({ command: { @@ -1508,6 +1553,13 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ }, when: ContextKeyExpr.and(TunnelPrivacyContextKey.isEqualTo(TunnelPrivacy.Public), isNotMultiSelectionExpr) })); +MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ + group: '2_localaddress', + order: 3, + submenu: MenuId.TunnelProtocol, + title: nls.localize('tunnelContext.protocolMenu', "Change Tunnel Protocol"), + when: ContextKeyExpr.and(isForwardedExpr, isNotMultiSelectionExpr) +})); MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '3_forward', order: 0, @@ -1526,6 +1578,23 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ }, })); +MenuRegistry.appendMenuItem(MenuId.TunnelProtocol, ({ + order: 0, + command: { + id: SetTunnelProtocolAction.ID_HTTP, + title: SetTunnelProtocolAction.LABEL_HTTP, + toggled: TunnelProtocolContextKey.isEqualTo(TunnelProtocol.Http) + } +})); +MenuRegistry.appendMenuItem(MenuId.TunnelProtocol, ({ + order: 1, + command: { + id: SetTunnelProtocolAction.ID_HTTPS, + title: SetTunnelProtocolAction.LABEL_HTTPS, + toggled: TunnelProtocolContextKey.isEqualTo(TunnelProtocol.Https) + } +})); + MenuRegistry.appendMenuItem(MenuId.TunnelPortInline, ({ group: '0_manage', diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 3fb24b9fff0..ce6f4704cda 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -11,7 +11,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ALL_INTERFACES_ADDRESSES, isAllInterfaces, isLocalhost, ITunnelService, LOCALHOST_ADDRESSES, PortAttributesProvider, ProvidedOnAutoForward, ProvidedPortAttributes, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEditableData } from 'vs/workbench/common/views'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TunnelInformation, TunnelDescription, IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -25,6 +25,7 @@ import { flatten } from 'vs/base/common/arrays'; import Severity from 'vs/base/common/severity'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; +import { deepClone } from 'vs/base/common/objects'; export const IRemoteExplorerService = createDecorator('remoteExplorerService'); export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; @@ -147,12 +148,17 @@ export enum OnPortForward { Ignore = 'ignore' } +export enum TunnelProtocol { + Http = 'http', + Https = 'https' +} + export interface Attributes { label: string | undefined; onAutoForward: OnPortForward | undefined, elevateIfNeeded: boolean | undefined; requireLocalPort: boolean | undefined; - protocol: string | undefined; + protocol: TunnelProtocol | undefined; } interface PortRange { start: number, end: number } @@ -331,6 +337,26 @@ export class PortsAttributes extends Disposable { default: return undefined; } } + + public async addAttributes(port: number, attributes: Partial) { + let settingValue = this.configurationService.inspect(PortsAttributes.SETTING); + const userValue: any = settingValue.userLocalValue; + let newUserValue: any; + if (!userValue || !isObject(userValue)) { + newUserValue = {}; + } else { + newUserValue = deepClone(userValue); + } + + if (!newUserValue[`${port}`]) { + newUserValue[`${port}`] = {}; + } + for (const attribute in attributes) { + newUserValue[`${port}`][attribute] = (attributes)[attribute]; + } + + return this.configurationService.updateValue(PortsAttributes.SETTING, newUserValue, ConfigurationTarget.USER_LOCAL); + } } const MISMATCH_LOCAL_PORT_COOLDOWN = 10 * 1000; // 10 seconds @@ -354,7 +380,7 @@ export class TunnelModel extends Disposable { private _onEnvironmentTunnelsSet: Emitter = new Emitter(); public onEnvironmentTunnelsSet: Event = this._onEnvironmentTunnelsSet.event; private _environmentTunnelsSet: boolean = false; - private configPortsAttributes: PortsAttributes; + public readonly configPortsAttributes: PortsAttributes; private restoreListener: IDisposable | undefined; private portAttributesProviders: PortAttributesProvider[] = []; From 615bb082c11ff21604b21c8c91b0706938e9c588 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 18 May 2021 15:12:54 +0200 Subject: [PATCH 030/270] Tunnel -> Port --- src/vs/workbench/contrib/remote/browser/tunnelView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 24ebccc190e..16a420a7ce3 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -1557,7 +1557,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '2_localaddress', order: 3, submenu: MenuId.TunnelProtocol, - title: nls.localize('tunnelContext.protocolMenu', "Change Tunnel Protocol"), + title: nls.localize('tunnelContext.protocolMenu', "Change Port Protocol"), when: ContextKeyExpr.and(isForwardedExpr, isNotMultiSelectionExpr) })); MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ From 8f78655dbe343a556b896997bd52a9288525f510 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 15:35:56 +0200 Subject: [PATCH 031/270] untitled file working copy - set visibility --- .../workingCopy/common/abstractFileWorkingCopyManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index f2f143e813e..5ee9c15ead3 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -139,7 +139,7 @@ export abstract class BaseFileWorkingCopyManager { + protected async save(resource: URI, options?: ISaveOptions): Promise { const workingCopy = this.get(resource); // Untitled: is always a "Save As" From 4726abdcfa8fda25b577b8628e1f9cc16eaf6339 Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 18 May 2021 16:05:09 +0200 Subject: [PATCH 032/270] explorer: do not remove dotes at end of filenames before validation fixes #117426 --- src/vs/workbench/contrib/files/browser/fileActions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index f569437b2c2..df99b2a38c8 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -681,7 +681,7 @@ function trimLongName(name: string): string { return name; } -export function getWellFormedFileName(filename: string): string { +function getWellFormedFileName(filename: string): string { if (!filename) { return filename; } @@ -689,8 +689,7 @@ export function getWellFormedFileName(filename: string): string { // Trim tabs filename = trim(filename, '\t'); - // Remove trailing dots and slashes - filename = rtrim(filename, '.'); + // Remove trailing slashes filename = rtrim(filename, '/'); filename = rtrim(filename, '\\'); From fb9d3f3e50114f93e5b9c7d3be49b3dee4725e83 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 18 May 2021 16:12:53 +0200 Subject: [PATCH 033/270] Fix all ports showing as user forwarded --- .../services/remote/common/remoteExplorerService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index ce6f4704cda..e2a6f46a8c9 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -363,6 +363,7 @@ const MISMATCH_LOCAL_PORT_COOLDOWN = 10 * 1000; // 10 seconds export class TunnelModel extends Disposable { readonly forwarded: Map; + private readonly inProgress: Map = new Map(); readonly detected: Map; private remoteTunnels: Map; private _onForwardPort: Emitter = new Emitter(); @@ -427,7 +428,9 @@ export class TunnelModel extends Disposable { this.detected = new Map(); this._register(this.tunnelService.onTunnelOpened(async (tunnel) => { const key = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); - if ((!this.forwarded.has(key)) && tunnel.localAddress) { + if (!mapHasAddressLocalhostOrAllInterfaces(this.forwarded, tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort) + && !mapHasAddressLocalhostOrAllInterfaces(this.inProgress, tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort) + && tunnel.localAddress) { const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); this.forwarded.set(key, { remoteHost: tunnel.tunnelRemoteHost, @@ -548,6 +551,8 @@ export class TunnelModel extends Disposable { getAddress: async () => { return (await this.remoteAuthorityResolverService.resolveAuthority(authority)).authority; } } : undefined; + const key = makeAddress(remote.host, remote.port); + this.inProgress.set(key, true); const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, localPort, (!elevateIfNeeded) ? attributes?.elevateIfNeeded : elevateIfNeeded, isPublic); if (tunnel && tunnel.localAddress) { const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), remote.host, remote.port); @@ -566,9 +571,9 @@ export class TunnelModel extends Disposable { privacy: this.makeTunnelPrivacy(tunnel.public), userForwarded: restore }; - const key = makeAddress(remote.host, remote.port); this.forwarded.set(key, newForward); this.remoteTunnels.set(key, tunnel); + this.inProgress.delete(key); await this.storeForwarded(); await this.showPortMismatchModalIfNeeded(tunnel, localPort, attributes); this._onForwardPort.fire(newForward); From 960af85cd93a6f6ae06840049b7d65f5fb445f00 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 18 May 2021 17:02:16 +0200 Subject: [PATCH 034/270] Reduce number of calls to get port attributes --- .../contrib/remote/browser/remoteExplorer.ts | 19 +++++++++++++------ .../remote/common/remoteExplorerService.ts | 11 ++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 88049494784..b437ae61a92 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Extensions, IViewContainersRegistry, IViewsRegistry, IViewsService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IRemoteExplorerService, makeAddress, mapHasAddressLocalhostOrAllInterfaces, OnPortForward, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, PORT_AUTO_SOURCE_SETTING_PROCESS, TUNNEL_VIEW_CONTAINER_ID, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { Attributes, IRemoteExplorerService, makeAddress, mapHasAddressLocalhostOrAllInterfaces, OnPortForward, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, PORT_AUTO_SOURCE_SETTING_PROCESS, TUNNEL_VIEW_CONTAINER_ID, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { forwardedPortsViewEnabled, ForwardPortAction, OpenPortInBrowserAction, TunnelPanel, TunnelPanelDescriptor, TunnelViewModel, OpenPortInPreviewAction } from 'vs/workbench/contrib/remote/browser/tunnelView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -443,13 +443,14 @@ class OutputAutomaticPortForwarding extends Disposable { if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, localUrl.host, localUrl.port)) { return; } - if ((await this.remoteExplorerService.tunnelModel.getAttributes([localUrl.port]))?.get(localUrl.port)?.onAutoForward === OnPortForward.Ignore) { + const attributes = (await this.remoteExplorerService.tunnelModel.getAttributes([localUrl.port]))?.get(localUrl.port); + if (attributes?.onAutoForward === OnPortForward.Ignore) { return; } if (this.privilegedOnly() && !isPortPrivileged(localUrl.port, (await this.remoteAgentService.getEnvironment())?.os)) { return; } - const forwarded = await this.remoteExplorerService.forward(localUrl, undefined, undefined, undefined, undefined, undefined, false); + const forwarded = await this.remoteExplorerService.forward(localUrl, undefined, undefined, undefined, undefined, undefined, false, attributes ?? null); if (forwarded) { this.notifier.doAction([forwarded]); } @@ -543,7 +544,7 @@ class ProcAutomaticPortForwarding extends Disposable { } private async forwardCandidates(): Promise { - const attributes = await this.remoteExplorerService.tunnelModel.getAttributes(this.remoteExplorerService.tunnelModel.candidates.map(candidate => candidate.port)); + let attributes: Map | undefined; const allTunnels: RemoteTunnel[] = []; for (const value of this.remoteExplorerService.tunnelModel.candidates) { if (!value.detail) { @@ -561,10 +562,16 @@ class ProcAutomaticPortForwarding extends Disposable { if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, value.host, value.port)) { continue; } - if (attributes?.get(value.port)?.onAutoForward === OnPortForward.Ignore) { + + if (!attributes) { + attributes = await this.remoteExplorerService.tunnelModel.getAttributes(this.remoteExplorerService.tunnelModel.candidates.map(candidate => candidate.port)); + } + + const portAttributes = attributes?.get(value.port); + if (portAttributes?.onAutoForward === OnPortForward.Ignore) { continue; } - const forwarded = await this.remoteExplorerService.forward(value, undefined, undefined, undefined, undefined, undefined, false); + const forwarded = await this.remoteExplorerService.forward(value, undefined, undefined, undefined, undefined, undefined, false, portAttributes ?? null); if (!alreadyForwarded && forwarded) { this.autoForwarded.add(address); } else if (forwarded) { diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index e2a6f46a8c9..efee4e0e4aa 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -540,9 +540,10 @@ export class TunnelModel extends Disposable { return this.dialogService.show(Severity.Info, mismatchString, [nls.localize('remote.localPortMismatch.Ok', "Ok")]); } - async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore: boolean = true, attributes?: Attributes): Promise { + async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, + isPublic?: boolean, restore: boolean = true, attributes?: Attributes | null): Promise { const existingTunnel = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, remote.host, remote.port); - attributes = attributes ?? (await this.getAttributes([remote.port]))?.get(remote.port); + attributes = attributes ?? ((attributes !== null) ? (await this.getAttributes([remote.port]))?.get(remote.port) : undefined); const localPort = (local !== undefined) ? local : remote.port; if (!existingTunnel) { @@ -813,7 +814,7 @@ export interface IRemoteExplorerService { onDidChangeEditable: Event<{ tunnel: ITunnelItem, editId: TunnelEditId } | undefined>; setEditable(tunnelItem: ITunnelItem | undefined, editId: TunnelEditId, data: IEditableData | null): void; getEditableData(tunnelItem: ITunnelItem | undefined, editId?: TunnelEditId): IEditableData | undefined; - forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean): Promise; + forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean, attributes?: Attributes | null): Promise; close(remote: { host: string, port: number }): Promise; setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void; setCandidateFilter(filter: ((candidates: CandidatePort[]) => Promise) | undefined): IDisposable; @@ -872,8 +873,8 @@ class RemoteExplorerService implements IRemoteExplorerService { return this._tunnelModel; } - forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean): Promise { - return this.tunnelModel.forward(remote, local, name, source, elevateIfNeeded, isPublic, restore); + forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore?: boolean, attributes?: Attributes | null): Promise { + return this.tunnelModel.forward(remote, local, name, source, elevateIfNeeded, isPublic, restore, attributes); } close(remote: { host: string, port: number }): Promise { From f113243a5164d8bce2418fad1061fc8f536af3af Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 18 May 2021 17:09:39 +0200 Subject: [PATCH 035/270] Close and reopen tunnel when protocol changes --- .../services/remote/common/remoteExplorerService.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index efee4e0e4aa..51210735438 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -583,11 +583,12 @@ export class TunnelModel extends Disposable { } else { if (attributes?.label ?? name) { existingTunnel.name = attributes?.label ?? name; + this._onForwardPort.fire(); } - if (attributes?.protocol) { - existingTunnel.localUri = this.makeLocalUri(existingTunnel.localAddress, attributes); + if (attributes?.protocol !== existingTunnel.localUri.scheme) { + await this.close(existingTunnel.remoteHost, existingTunnel.remotePort); + await this.forward({ host: existingTunnel.remoteHost, port: existingTunnel.remotePort }, local, name, source, elevateIfNeeded, isPublic, restore, attributes); } - this._onForwardPort.fire(); return mapHasAddressLocalhostOrAllInterfaces(this.remoteTunnels, remote.host, remote.port); } } From daaa8a98bf3a32d5745577d66782b406d8468e0e Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 18 May 2021 17:16:23 +0200 Subject: [PATCH 036/270] debug: focus breakpoint on debug stop #118743 --- .../contrib/debug/browser/breakpointsView.ts | 13 +++++++++++++ src/vs/workbench/contrib/debug/common/debug.ts | 1 + 2 files changed, 14 insertions(+) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index e0f4fa1c42a..7b8df83d52e 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -102,6 +102,7 @@ export class BreakpointsView extends ViewPane { this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); + this._register(this.debugService.onDidChangeState(() => this.onStateChange())); } override renderBody(container: HTMLElement): void { @@ -254,6 +255,18 @@ export class BreakpointsView extends ViewPane { } } + private onStateChange(): void { + const thread = this.debugService.getViewModel().focusedThread; + if (thread && thread.stoppedDetails && thread.stoppedDetails.hitBreakpointIds && thread.stoppedDetails.hitBreakpointIds.length > 0) { + const hitBreakpointIds = thread.stoppedDetails.hitBreakpointIds; + const elements = this.elements; + const index = elements.findIndex(e => e.getIdFromAdapter(thread.session.getId()) === hitBreakpointIds[0]); + if (index >= 0) { + this.list.setFocus([index]); + } + } + } + private get elements(): BreakpointItem[] { const model = this.debugService.getModel(); const elements = (>model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getDataBreakpoints()).concat(model.getBreakpoints()); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index c005f81bd2a..3a46f18c937 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -106,6 +106,7 @@ export interface IRawStoppedDetails { totalFrames?: number; allThreadsStopped?: boolean; framesErrorMessage?: string; + hitBreakpointIds?: number[]; } // model From e02714b57756ab391e6295bbfe3e9c8f28998323 Mon Sep 17 00:00:00 2001 From: Raymond Zhao Date: Tue, 18 May 2021 08:56:03 -0700 Subject: [PATCH 037/270] Polish for some workspace reuse (#123519) * Behave as if Terminate was received when the renderer socket is closed * Simplify search service dispose Co-authored-by: Alex Dima --- .../extensions/electron-browser/localProcessExtensionHost.ts | 2 ++ .../services/extensions/node/extensionHostProcessSetup.ts | 3 +++ .../services/search/electron-browser/searchService.ts | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 72535eae553..a29459cf692 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -627,6 +627,8 @@ export class LocalProcessExtensionHost implements IExtensionHost { // (graceful termination) protocol.send(createMessageOfType(MessageType.Terminate)); + protocol.getSocket().dispose(); + protocol.dispose(); // Give the extension host 10s, after which we will diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index 96ab14ef3ca..a0baef25034 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -173,6 +173,9 @@ function _createExtHostProtocol(): Promise { }); socket.once('error', reject); + socket.on('close', () => { + onTerminate('renderer closed the socket'); + }); }); } } diff --git a/src/vs/workbench/services/search/electron-browser/searchService.ts b/src/vs/workbench/services/search/electron-browser/searchService.ts index 3a667048048..3e742a28ace 100644 --- a/src/vs/workbench/services/search/electron-browser/searchService.ts +++ b/src/vs/workbench/services/search/electron-browser/searchService.ts @@ -27,6 +27,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { FileAccess } from 'vs/base/common/network'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class LocalSearchService extends SearchService { constructor( @@ -54,6 +55,7 @@ export class DiskSearch implements ISearchResultProvider { searchDebug: IDebugParams | undefined, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configService: IConfigurationService, + @ILifecycleService private readonly lifecycleService: ILifecycleService ) { const timeout = this.configService.getValue().search.maintainFileSearchCache ? 100 * 60 * 60 * 1000 : @@ -84,6 +86,8 @@ export class DiskSearch implements ISearchResultProvider { const client = new Client(FileAccess.asFileUri('bootstrap-fork', require).fsPath, opts); const channel = getNextTickChannel(client.getChannel('search')); this.raw = new SearchChannelClient(channel); + + this.lifecycleService.onWillShutdown(_ => client.dispose()); } textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise { From aee486cd293a8d060fe01200286e462ee2e094a2 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 18:28:35 +0200 Subject: [PATCH 038/270] untitled file working copy - poperly resolve target --- .../dialogs/test/common/testDialogService.ts | 23 ++- .../common/abstractFileWorkingCopyManager.ts | 84 +++++------ .../common/fileWorkingCopyManager.ts | 27 +++- .../common/untitledFileWorkingCopyManager.ts | 23 ++- .../test/browser/fileWorkingCopy.test.ts | 8 +- .../browser/fileWorkingCopyManager.test.ts | 3 +- .../test/browser/resourceWorkingCopyTest.ts | 2 +- .../untitledFileWorkingCopyManager.test.ts | 132 +++++++++++++++++- .../test/browser/workbenchTestServices.ts | 5 +- 9 files changed, 231 insertions(+), 76 deletions(-) diff --git a/src/vs/platform/dialogs/test/common/testDialogService.ts b/src/vs/platform/dialogs/test/common/testDialogService.ts index 8d3d2465b76..1d2b2f08f71 100644 --- a/src/vs/platform/dialogs/test/common/testDialogService.ts +++ b/src/vs/platform/dialogs/test/common/testDialogService.ts @@ -10,8 +10,23 @@ export class TestDialogService implements IDialogService { declare readonly _serviceBrand: undefined; - confirm(_confirmation: IConfirmation): Promise { return Promise.resolve({ confirmed: false }); } - show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise { return Promise.resolve({ choice: 0 }); } - input(): Promise { { return Promise.resolve({ choice: 0, values: [] }); } } - about(): Promise { return Promise.resolve(); } + private confirmResult: IConfirmationResult | undefined = undefined; + setConfirmResult(result: IConfirmationResult) { + this.confirmResult = result; + } + + async confirm(confirmation: IConfirmation): Promise { + if (this.confirmResult) { + const confirmResult = this.confirmResult; + this.confirmResult = undefined; + + return confirmResult; + } + + return { confirmed: false }; + } + + async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { return { choice: 0 }; } + async input(): Promise { { return { choice: 0, values: [] }; } } + async about(): Promise { } } diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 5ee9c15ead3..337afbf2dae 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -12,7 +12,8 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; -import { FileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { FileWorkingCopy, IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { IConfirmation, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { basename, dirname, isEqual, joinPath, toLocalResource } from 'vs/base/common/resources'; @@ -59,8 +60,8 @@ export interface IBaseFileWorkingCopyManager; - saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise; + saveAs(source: URI, target: URI, options?: ISaveOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined>; /** * Disposes all working copies of the manager and disposes the manager. This @@ -90,6 +91,7 @@ export abstract class BaseFileWorkingCopyManager | undefined { const workingCopy = this.workingCopyService.get({ resource, typeId: this.workingCopyTypeId }); if (workingCopy instanceof FileWorkingCopy) { - return workingCopy as unknown as W; + return workingCopy; } return undefined; @@ -139,45 +141,16 @@ export abstract class BaseFileWorkingCopyManager { - const workingCopy = this.get(resource); - - // Untitled: is always a "Save As" - if (workingCopy instanceof UntitledFileWorkingCopy) { - let targetUri: URI | undefined; - - // Untitled with associated file path is taken as is - if (workingCopy.hasAssociatedFilePath) { - targetUri = await this.suggestSavePath(resource); - } - - // Otherwise ask user for a target path to save to - else { - targetUri = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(resource), options?.availableFileSystems); - } - - // Save as if target provided - if (targetUri) { - return this.saveAs(resource, targetUri, options); - } - } - - // File: via save method of working copy - else if (workingCopy) { - const success = await workingCopy.save(options); - if (success) { - return workingCopy; - } - } - - return undefined; - } - - async saveAs(source: URI, target?: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise { + async saveAs(source: URI, target?: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined> { // Get to target resource if (!target) { - target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + const workingCopy = this.get(source); + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + target = await this.suggestSavePath(source); + } else { + target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + } } if (!target) { @@ -187,7 +160,7 @@ export abstract class BaseFileWorkingCopyManager { + private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { + + // Save is only possible with file working copies, + // any other have to go via `saveAs` flow. + const fileWorkingCopy = this.getFile(resource); + if (fileWorkingCopy) { + const success = await fileWorkingCopy.save(options); + if (success) { + return fileWorkingCopy; + } + } + + return undefined; + } + + private async doSaveAs(source: URI, target: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined> { let sourceContents: VSBufferReadableStream; // If the source is an existing file working copy, we can directly @@ -256,7 +244,7 @@ export abstract class BaseFileWorkingCopyManager { + private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetFileWorkingCopy: IFileWorkingCopy }> { // Prefer an existing file working copy if it is already resolved // for the given target resource @@ -283,17 +271,15 @@ export abstract class BaseFileWorkingCopyManager; - private async confirmOverwrite(resource: URI): Promise { const confirm: IConfirmation = { message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 80e427d8867..ebbbb29a82e 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -129,6 +129,14 @@ export interface IFileWorkingCopyResolveOptions { }; } +export interface IFileWorkingCopyResolver { + + /** + * A delegate to resolve a file working copy. + */ + (resource: URI): Promise>; +} + export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events @@ -176,7 +184,20 @@ export class FileWorkingCopyManager extends Bas @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IPathService pathService: IPathService ) { - super(workingCopyTypeId, fileService, logService, workingCopyBackupService, fileDialogService, uriIdentityService, workingCopyFileService, dialogService, workingCopyService, environmentService, pathService); + super( + workingCopyTypeId, + resource => this.resolve(resource), + fileService, + logService, + workingCopyBackupService, + fileDialogService, + uriIdentityService, + workingCopyFileService, + dialogService, + workingCopyService, + environmentService, + pathService + ); this.registerListeners(); } @@ -471,10 +492,6 @@ export class FileWorkingCopyManager extends Bas } } - protected doResolve(resource: URI): Promise> { - return this.resolve(resource); - } - private joinPendingResolve(resource: URI): Promise | undefined { const pendingWorkingCopyResolve = this.mapResourceToPendingWorkingCopyResolve.get(resource); if (pendingWorkingCopyResolve) { diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 344c038662f..72f230c634d 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -21,6 +21,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; /** * The only one that should be dealing with `IUntitledFileWorkingCopy` and @@ -104,6 +105,7 @@ export class UntitledFileWorkingCopyManager, + fileWorkingCopyResolver: IFileWorkingCopyResolver, @IFileService fileService: IFileService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -117,7 +119,20 @@ export class UntitledFileWorkingCopyManager fileWorkingCopyResolver(resource), + fileService, + logService, + workingCopyBackupService, + fileDialogService, + uriIdentityService, + workingCopyFileService, + dialogService, + workingCopyService, + environmentService, + pathService + ); } //#region Resolve @@ -132,10 +147,6 @@ export class UntitledFileWorkingCopyManager> { - return this.resolve({ untitledResource: resource }); - } - private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { const massagedOptions = this.massageOptions(options); @@ -198,7 +209,7 @@ export class UntitledFileWorkingCopyManager (async () => { - const result = await this.save(workingCopy.resource, options); + const result = await this.saveAs(workingCopy.resource, undefined, options); return result ? true : false; })() diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index 81ef604cc9e..e01b543faa1 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -128,7 +128,7 @@ suite('FileWorkingCopy', function () { assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); let onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await onDidChangeOrphanedPromise; @@ -291,7 +291,7 @@ suite('FileWorkingCopy', function () { const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; @@ -318,7 +318,7 @@ suite('FileWorkingCopy', function () { const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; @@ -464,7 +464,7 @@ suite('FileWorkingCopy', function () { // save clears orphaned const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index d730021d380..cbc543d430a 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -26,10 +26,9 @@ suite('FileWorkingCopyManager', () => { instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); - const factory = new TestFileWorkingCopyModelFactory(); manager = new FileWorkingCopyManager( 'testFileWorkingCopyType', - factory, + new TestFileWorkingCopyModelFactory(), accessor.fileService, accessor.lifecycleService, accessor.labelService, diff --git a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts index 66376a44b79..e3fdc5f46b7 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts @@ -53,7 +53,7 @@ suite('ResourceWorkingCopy', function () { assert.strictEqual(workingCopy.isOrphaned(), false); let onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await onDidChangeOrphanedPromise; diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 424426b8760..28119fb7798 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -8,10 +8,12 @@ import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; -import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestInMemoryFileSystemProvider, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('UntitledFileWorkingCopyManager', () => { @@ -19,15 +21,37 @@ suite('UntitledFileWorkingCopyManager', () => { let accessor: TestServiceAccessor; let manager: IUntitledFileWorkingCopyManager; + let fileManager: IFileWorkingCopyManager; setup(() => { instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); - const factory = new TestUntitledFileWorkingCopyModelFactory(); + accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); + accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); + + fileManager = new FileWorkingCopyManager( + 'testFileWorkingCopyType', + new TestFileWorkingCopyModelFactory(), + accessor.fileService, + accessor.lifecycleService, + accessor.labelService, + instantiationService, + accessor.logService, + accessor.fileDialogService, + accessor.workingCopyFileService, + accessor.workingCopyBackupService, + accessor.uriIdentityService, + accessor.dialogService, + accessor.workingCopyService, + accessor.environmentService, + accessor.pathService + ); + manager = new UntitledFileWorkingCopyManager( 'testUntitledWorkingCopyType', - factory, + new TestUntitledFileWorkingCopyModelFactory(), + resource => fileManager.resolve(resource), accessor.fileService, accessor.labelService, instantiationService, @@ -141,6 +165,108 @@ suite('UntitledFileWorkingCopyManager', () => { assert.strictEqual(workingCopy.hasAssociatedFilePath, true); assert.strictEqual(workingCopy.resource.path, '/some/associated.txt'); + + workingCopy.dispose(); + }); + + test('save - without associated resource', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save'); + + accessor.fileDialogService.setPickFileToSave(URI.file('simple/file.txt')); + + const result = await workingCopy.save(); + assert.ok(result); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('save - with associated resource', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save with associated resource'); + + accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }), true); + + const result = await workingCopy.save(); + assert.ok(result); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('save - with associated resource (asks to overwrite)', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save with associated resource'); + + let result = await workingCopy.save(); + assert.ok(!result); // not confirmed + + assert.strictEqual(manager.get(workingCopy.resource), workingCopy); + + accessor.dialogService.setConfirmResult({ confirmed: true }); + + result = await workingCopy.save(); + assert.ok(result); // confirmed + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - without associated resource', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - with associated resource', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save As with associated resource'); + + const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); + + accessor.fileService.notExistsSet.set(target, true); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - target exists and is resolved', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + const targetFileWorkingCopy = await fileManager.resolve(target); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result, targetFileWorkingCopy); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); }); test('destroy', async () => { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 31a0a9caab2..6ea06def96d 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -138,6 +138,7 @@ import { IElevatedFileService } from 'vs/workbench/services/files/common/elevate import { BrowserElevatedFileService } from 'vs/workbench/services/files/browser/elevatedFileService'; import { IDiffComputationResult, IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/modes'; +import { ResourceMap } from 'vs/base/common/map'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined); @@ -261,7 +262,7 @@ export class TestServiceAccessor { @IModelService public modelService: ModelServiceImpl, @IFileService public fileService: TestFileService, @IFileDialogService public fileDialogService: TestFileDialogService, - @IDialogService public dialogService: IDialogService, + @IDialogService public dialogService: TestDialogService, @IWorkingCopyService public workingCopyService: IWorkingCopyService, @IEditorService public editorService: TestEditorService, @IWorkbenchEnvironmentService public environmentService: IWorkbenchEnvironmentService, @@ -870,7 +871,7 @@ export class TestFileService implements IFileService { return stats.map(stat => ({ stat, success: true })); } - readonly notExistsSet = new Set(); + readonly notExistsSet = new ResourceMap(); async exists(_resource: URI): Promise { return !this.notExistsSet.has(_resource); } From 455a8b6949bb01358f74bc5f8980ff8bc81052df Mon Sep 17 00:00:00 2001 From: Miguel Solorio Date: Tue, 18 May 2021 10:24:01 -0700 Subject: [PATCH 039/270] List focus colors are too dark (Fixes #123703) --- extensions/theme-defaults/themes/light_vs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 5f59dbe7b77..b58b9f88321 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -25,7 +25,7 @@ "notebook.cellBorderColor": "#E8E8E8", "notebook.selectedCellBackground": "#c8ddf150", "statusBarItem.errorBackground": "#c72e0f", - "list.focusHighlightForeground": "#33B6FF" + "list.focusHighlightForeground": "#9DDDFF" }, "tokenColors": [ { From fc76d8b6b441047e268d495b66308172afe56ccd Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 18 May 2021 17:40:30 +0000 Subject: [PATCH 040/270] fix #124113 --- src/vs/platform/terminal/node/ptyService.ts | 2 +- src/vs/platform/terminal/node/terminalProcess.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 6951a9c4bc4..1d06ff418eb 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -377,7 +377,7 @@ export class PersistentTerminalProcess extends Disposable { } this._isStarted = true; } else { - this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() <= 19041 }); + this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); this._onProcessTitleChanged.fire(this._terminalProcess.currentTitle); this._onProcessShellTypeChanged.fire(this._terminalProcess.shellType); this.triggerReplay(); diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 4fca95085ee..df0737399af 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -324,7 +324,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } private _sendProcessId(pid: number) { - this._onProcessReady.fire({ pid, cwd: this._initialCwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() <= 19041 }); + this._onProcessReady.fire({ pid, cwd: this._initialCwd, requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376 }); } private _sendProcessTitle(ptyProcess: pty.IPty): void { From 34775b7d660a5bb101d0627b6837e760d120e5c1 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Tue, 18 May 2021 10:49:17 -0700 Subject: [PATCH 041/270] add requestOpenUris api --- .../workspace/common/workspaceTrust.ts | 1 + .../workspaces/common/workspaceTrust.ts | 46 +++++++++++++++++++ .../test/common/testWorkspaceTrustService.ts | 4 ++ 3 files changed, 51 insertions(+) diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 0c6c469cfaa..ccfc97c13b0 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -60,6 +60,7 @@ export interface IWorkspaceTrustRequestService { readonly onDidInitiateWorkspaceTrustRequest: Event; + requestOpenUris(uris: URI[]): Promise; cancelRequest(): void; completeRequest(trusted?: boolean): Promise; requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 36823d3132e..cc447a65296 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -9,10 +9,12 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { LinkedList } from 'vs/base/common/linkedList'; import { Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; +import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -317,6 +319,7 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa _serviceBrand: undefined; private _trusted!: boolean; + private _acceptingNonWorkspaceFiles!: boolean; private _modalTrustRequestPromise?: Promise; private _modalTrustRequestResolver?: (trusted: boolean | undefined) => void; private readonly _ctxWorkspaceTrustState: IContextKey; @@ -327,6 +330,7 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa constructor( @IContextKeyService contextKeyService: IContextKeyService, + @IDialogService private readonly dialogService: IDialogService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, ) { super(); @@ -336,6 +340,8 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa this._ctxWorkspaceTrustState = WorkspaceTrustContext.IsTrusted.bindTo(contextKeyService); this.trusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); + + this._acceptingNonWorkspaceFiles = false; } private get trusted(): boolean { @@ -343,6 +349,10 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa } private set trusted(trusted: boolean) { + if (!trusted) { + this._acceptingNonWorkspaceFiles = false; + } + this._trusted = trusted; this._ctxWorkspaceTrustState.set(trusted); } @@ -356,6 +366,42 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa } } + async requestOpenUris(uris: URI[]): Promise { + // If workspace is untrusted, there is no conflict + if (!this.trusted) { + return true; + } + + const allTrusted = uris.map(uri => { + return this.workspaceTrustManagementService.getUriTrustInfo(uri).trusted; + }).every(trusted => trusted); + + // If all uris are trusted, there is no conflict + if (allTrusted) { + return true; + } + + // If we already asked the user, don't need to ask again + if (this._acceptingNonWorkspaceFiles) { + return true; + } + + const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Are you sure you want to open these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Window"), localize('cancel', "Cancel")], { + detail: localize('openLooseFileDetails', "You are trying to open untrusted files into the current window which is trusted. How would you like to continue?"), + cancelId: 2, + }); + + switch (result.choice) { + case 0: + this._acceptingNonWorkspaceFiles = true; + return true; + case 1: + return false; + default: + return undefined; + } + } + cancelRequest(): void { if (this._modalTrustRequestResolver) { this._modalTrustRequestResolver(undefined); diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts index 15a8259c97a..deac598655b 100644 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts @@ -79,6 +79,10 @@ export class TestWorkspaceTrustRequestService implements IWorkspaceTrustRequestS constructor(private readonly _trusted: boolean) { } + async requestOpenUris(uris: URI[]): Promise { + throw new Error('Method not implemented.'); + } + cancelRequest(): void { throw new Error('Method not implemented.'); } From 4e7029f593b53783cfa833c920f662131d6c746a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Tue, 18 May 2021 20:05:49 +0200 Subject: [PATCH 042/270] workbench web api: builtinExtensionsFilter --- .../common/webExtensionsScannerService.ts | 14 +++++++++++--- src/vs/workbench/workbench.web.api.ts | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index f8faa18b51c..14b42deed94 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -25,7 +25,7 @@ import { Event } from 'vs/base/common/event'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { localize } from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; -import { isArray } from 'vs/base/common/types'; +import { isArray, isFunction } from 'vs/base/common/types'; interface IUserExtension { identifier: IExtensionIdentifier; @@ -76,8 +76,16 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } private async readSystemExtensions(): Promise { - const extensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions(); - return extensions.concat(this.getStaticExtensions(true)); + let [builtinExtensions, staticExtensions] = await Promise.all([ + this.builtinExtensionsScannerService.scanBuiltinExtensions(), + this.getStaticExtensions(true) + ]); + + if (isFunction(this.environmentService.options?.builtinExtensionsFilter)) { + builtinExtensions = builtinExtensions.filter(e => this.environmentService.options!.builtinExtensionsFilter!(e.identifier.id)); + } + + return [...builtinExtensions, ...staticExtensions]; } /** diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 0605863599d..f6df1c777f2 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -321,6 +321,11 @@ interface IWorkbenchConstructionOptions { */ readonly staticExtensions?: readonly IStaticExtension[]; + /** + * Filter for built-in extensions. + */ + readonly builtinExtensionsFilter?: (extensionId: string) => boolean; + /** * [TEMPORARY]: This will be removed soon. * Enable inlined extensions. From 1243a51d59bafbd96dbeeb5008e75023fdb52c03 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 18 May 2021 11:15:02 -0700 Subject: [PATCH 043/270] Support join terminals Fixes #122286 --- .../contrib/terminal/browser/terminal.ts | 9 +++- .../terminal/browser/terminalActions.ts | 19 ++++++- .../contrib/terminal/browser/terminalGroup.ts | 23 ++++----- .../contrib/terminal/browser/terminalMenus.ts | 36 +++++++++----- .../terminal/browser/terminalService.ts | 49 +++++++++++++++++++ .../terminal/browser/terminalTabsList.ts | 13 +++-- .../contrib/terminal/common/terminal.ts | 1 + 7 files changed, 116 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 495bbcffcdb..3edf41f34a6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -64,14 +64,18 @@ export interface ITerminalGroup { activeInstance: ITerminalInstance | null; terminalInstances: ITerminalInstance[]; title: string; - onDisposed: Event; - onInstancesChanged: Event; + + readonly onDisposed: Event; + readonly onInstancesChanged: Event; + readonly onPanelOrientationChanged: Event; + focusPreviousPane(): void; focusNextPane(): void; resizePane(direction: Direction): void; resizePanes(relativeSizes: number[]): void; setActiveInstanceByIndex(index: number): void; attachToElement(element: HTMLElement): void; + addInstance(instance: ITerminalInstance): void; removeInstance(instance: ITerminalInstance): void; setVisible(visible: boolean): void; layout(width: number, height: number): void; @@ -145,6 +149,7 @@ export interface ITerminalService { splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig, cwd?: string | URI): ITerminalInstance | null; splitInstance(instance: ITerminalInstance, profile: ITerminalProfile): ITerminalInstance | null; unsplitInstance(instance: ITerminalInstance): void; + joinInstances(instances: ITerminalInstance[]): void; /** * Perform an action with the active terminal instance, if the terminal does diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 6c1b44b85ba..497aef6b57e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -1392,7 +1392,6 @@ export function registerTerminalActions() { }); } } - return undefined; } }); registerAction2(class extends Action2 { @@ -1410,6 +1409,24 @@ export function registerTerminalActions() { await terminalService.doWithActiveInstance(async t => terminalService.unsplitInstance(t)); } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.JoinInstance, + title: { value: localize('workbench.action.terminal.joinInstance', "Join Terminals"), original: 'Join Terminals' }, + f1: true, + category, + precondition: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION.toNegated()) + }); + } + async run(accessor: ServicesAccessor) { + const terminalService = accessor.get(ITerminalService); + const instances = getSelectedInstances(accessor); + if (instances) { + terminalService.joinInstances(instances); + } + } + }); registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index d91b2448225..46b266289fa 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -248,6 +248,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { readonly onInstancesChanged: Event = this._onInstancesChanged.event; private readonly _onPanelOrientationChanged = new Emitter(); get onPanelOrientationChanged(): Event { return this._onPanelOrientationChanged.event; } + constructor( private _container: HTMLElement | undefined, shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance | undefined, @@ -274,13 +275,19 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } else { instance = this._terminalService.createInstance(shellLaunchConfigOrInstance); } - this._terminalInstances.push(instance); + if (this._terminalInstances.length === 0) { + this._terminalInstances.push(instance); + } else { + this._terminalInstances.splice(this._activeInstanceIndex + 1, 0, instance); + } this._initInstanceListeners(instance); if (this._splitPaneContainer) { this._splitPaneContainer!.split(instance); } + instance.setVisible(this._isVisible); + this._onInstancesChanged.fire(); } @@ -317,7 +324,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { }; } - private _initInstanceListeners(instance: ITerminalInstance): void { + private _initInstanceListeners(instance: ITerminalInstance) { this._instanceDisposables.set(instance.instanceId, [ instance.onDisposed(instance => this._onInstanceDisposed(instance)), instance.onFocused(instance => this._setActiveInstance(instance)) @@ -328,7 +335,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._removeInstance(instance); } - removeInstance(instance: ITerminalInstance): void { + removeInstance(instance: ITerminalInstance) { this._removeInstance(instance); // Dispose instance event listeners @@ -372,7 +379,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } } - private _setActiveInstance(instance: ITerminalInstance): void { + private _setActiveInstance(instance: ITerminalInstance) { this.setActiveInstanceByIndex(this._getIndexFromId(instance.instanceId)); } @@ -456,14 +463,8 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { split(shellLaunchConfig: IShellLaunchConfig): ITerminalInstance { const instance = this._terminalService.createInstance(shellLaunchConfig); - this._terminalInstances.splice(this._activeInstanceIndex + 1, 0, instance); - this._initInstanceListeners(instance); + this.addInstance(instance); this._setActiveInstance(instance); - - if (this._splitPaneContainer) { - this._splitPaneContainer.split(instance, this._activeInstanceIndex); - } - return instance; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 1a0caeb3c89..6003a55d239 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyAndExpr, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, TerminalCommandId, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION, TerminalCommandId, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; const enum ContextMenuGroup { Create = '1_create', @@ -228,6 +228,15 @@ export function setupTerminalMenus(): void { MenuRegistry.appendMenuItems( [ + { + id: MenuId.TerminalInlineTabContext, item: { + group: ContextMenuGroup.Create, + command: { + id: TerminalCommandId.Split, + title: localize('workbench.action.terminal.split', "Split Terminal") + } + } + }, { id: MenuId.TerminalInlineTabContext, item: { command: { @@ -257,15 +266,6 @@ export function setupTerminalMenus(): void { group: ContextMenuGroup.Edit } }, - { - id: MenuId.TerminalInlineTabContext, item: { - group: ContextMenuGroup.Create, - command: { - id: TerminalCommandId.Split, - title: localize('workbench.action.terminal.split', "Split Terminal") - } - } - }, { id: MenuId.TerminalInlineTabContext, item: { command: { @@ -280,6 +280,15 @@ export function setupTerminalMenus(): void { MenuRegistry.appendMenuItems( [ + { + id: MenuId.TerminalTabContext, item: { + command: { + id: TerminalCommandId.SplitInstance, + title: localize('workbench.action.terminal.splitInstance', "Split Terminal"), + }, + group: ContextMenuGroup.Create + } + }, { id: MenuId.TerminalTabContext, item: { command: { @@ -309,11 +318,12 @@ export function setupTerminalMenus(): void { }, { id: MenuId.TerminalTabContext, item: { + group: ContextMenuGroup.Config, command: { - id: TerminalCommandId.SplitInstance, - title: localize('workbench.action.terminal.splitInstance', "Split Terminal"), + id: TerminalCommandId.JoinInstance, + title: localize('workbench.action.terminal.joinInstance', "Join Terminals") }, - group: ContextMenuGroup.Create + when: KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION.toNegated() } }, { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 86e6c28143e..3fb23678d6a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -638,6 +638,55 @@ export class TerminalService implements ITerminalService { this._onInstancesChanged.fire(); } + joinInstances(instances: ITerminalInstance[]): void { + // Find the group of the first instance that is the only instance in the group, if one exists + let candidateInstance: ITerminalInstance | undefined = undefined; + let candidateGroup: ITerminalGroup | undefined = undefined; + for (const instance of instances) { + const group = this.getGroupForInstance(instance); + if (group?.terminalInstances.length === 1) { + candidateInstance = instance; + candidateGroup = group; + break; + } + } + + // Create a new group if needed + if (!candidateGroup) { + candidateGroup = this._instantiationService.createInstance(TerminalGroup, this._terminalContainer, undefined); + candidateGroup.onPanelOrientationChanged((orientation) => this._onPanelOrientationChanged.fire(orientation)); + this._terminalGroups.push(candidateGroup); + candidateGroup.addDisposable(candidateGroup.onDisposed(this._onGroupDisposed.fire, this._onGroupDisposed)); + candidateGroup.addDisposable(candidateGroup.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); + } + + const wasActiveGroup = this.getActiveGroup() === candidateGroup; + + // Unsplit all other instances and add them to the new group + for (const instance of instances) { + if (instance === candidateInstance) { + continue; + } + + const oldGroup = this.getGroupForInstance(instance); + if (!oldGroup) { + // Something went wrong, don't join this one + continue; + } + oldGroup.removeInstance(instance); + candidateGroup.addInstance(instance); + } + + // Set the active terminal + this.setActiveInstance(instances[0]); + + // Fire events + this._onInstancesChanged.fire(); + if (!wasActiveGroup) { + this._onActiveGroupChanged.fire(); + } + } + protected _initInstanceListeners(instance: ITerminalInstance): void { instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed)); instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 34b1151d9f3..5522e60a32c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -133,13 +133,8 @@ export class TerminalTabList extends WorkbenchList { this._terminalTabsSingleSelectedContextKey = KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION.bindTo(contextKeyService); - this.onDidChangeSelection(e => { - this._terminalTabsSingleSelectedContextKey.set(e.elements.length === 1); - }); - - this.onDidChangeFocus(e => { - this._terminalTabsSingleSelectedContextKey.set(e.elements.length === 1); - }); + this.onDidChangeSelection(e => this._updateContextKey()); + this.onDidChangeFocus(() => this._updateContextKey()); this.onDidOpen(async e => { const instance = e.element; @@ -161,6 +156,10 @@ export class TerminalTabList extends WorkbenchList { render(): void { this.splice(0, this.length, this._terminalService.terminalInstances); } + + private _updateContextKey() { + this._terminalTabsSingleSelectedContextKey.set(this.getSelectedElements().length === 1); + } } class TerminalTabsRenderer implements IListRenderer { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 6ec4d1f8ab7..9386ac00020 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -398,6 +398,7 @@ export const enum TerminalCommandId { SplitInstance = 'workbench.action.terminal.splitInstance', SplitInActiveWorkspace = 'workbench.action.terminal.splitInActiveWorkspace', Unsplit = 'workbench.action.terminal.unsplit', + JoinInstance = 'workbench.action.terminal.joinInstance', Relaunch = 'workbench.action.terminal.relaunch', FocusPreviousPane = 'workbench.action.terminal.focusPreviousPane', ShowTabs = 'workbench.action.terminal.showTabs', From 21e0fab8b87ee7034d09cb5498d0a70ca640c716 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 18 May 2021 11:16:16 -0700 Subject: [PATCH 044/270] Don't show join in command palette --- src/vs/workbench/contrib/terminal/browser/terminalActions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 497aef6b57e..c0a7b7ae945 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -1414,7 +1414,6 @@ export function registerTerminalActions() { super({ id: TerminalCommandId.JoinInstance, title: { value: localize('workbench.action.terminal.joinInstance', "Join Terminals"), original: 'Join Terminals' }, - f1: true, category, precondition: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION.toNegated()) }); From 71f4934a0c674589cc402bddbe8e05f374075b53 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Tue, 18 May 2021 11:19:38 -0700 Subject: [PATCH 045/270] use enum --- .../platform/workspace/common/workspaceTrust.ts | 8 +++++++- .../services/workspaces/common/workspaceTrust.ts | 16 ++++++++-------- .../test/common/testWorkspaceTrustService.ts | 4 ++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index ccfc97c13b0..06554ed2cb5 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -53,6 +53,12 @@ export interface IWorkspaceTrustManagementService { setTrustedFolders(folders: URI[]): Promise; } +export const enum WorkspaceTrustUriResponse { + Open = 1, + OpenInNewWindow = 2, + Cancel = 3 +} + export const IWorkspaceTrustRequestService = createDecorator('workspaceTrustRequestService'); export interface IWorkspaceTrustRequestService { @@ -60,7 +66,7 @@ export interface IWorkspaceTrustRequestService { readonly onDidInitiateWorkspaceTrustRequest: Event; - requestOpenUris(uris: URI[]): Promise; + requestOpenUris(uris: URI[]): Promise; cancelRequest(): void; completeRequest(trusted?: boolean): Promise; requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index cc447a65296..c49d8f6fade 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -19,7 +19,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; import { isSingleFolderWorkspaceIdentifier, isUntitledWorkspace, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; @@ -366,10 +366,10 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa } } - async requestOpenUris(uris: URI[]): Promise { + async requestOpenUris(uris: URI[]): Promise { // If workspace is untrusted, there is no conflict if (!this.trusted) { - return true; + return WorkspaceTrustUriResponse.Open; } const allTrusted = uris.map(uri => { @@ -378,12 +378,12 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa // If all uris are trusted, there is no conflict if (allTrusted) { - return true; + return WorkspaceTrustUriResponse.Open; } // If we already asked the user, don't need to ask again if (this._acceptingNonWorkspaceFiles) { - return true; + return WorkspaceTrustUriResponse.Open; } const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Are you sure you want to open these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Window"), localize('cancel', "Cancel")], { @@ -394,11 +394,11 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa switch (result.choice) { case 0: this._acceptingNonWorkspaceFiles = true; - return true; + return WorkspaceTrustUriResponse.Open; case 1: - return false; + return WorkspaceTrustUriResponse.OpenInNewWindow; default: - return undefined; + return WorkspaceTrustUriResponse.Cancel; } } diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts index deac598655b..4f25ee6c78e 100644 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts @@ -6,7 +6,7 @@ import { Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo, WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo, WorkspaceTrustRequestOptions, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManagementService { @@ -79,7 +79,7 @@ export class TestWorkspaceTrustRequestService implements IWorkspaceTrustRequestS constructor(private readonly _trusted: boolean) { } - async requestOpenUris(uris: URI[]): Promise { + async requestOpenUris(uris: URI[]): Promise { throw new Error('Method not implemented.'); } From 2bc36bec9dc4f750c3c48da36a4c81e55e6b9abe Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 18 May 2021 20:23:22 +0200 Subject: [PATCH 046/270] breakpoints select on stop fixes #118743 --- src/vs/workbench/contrib/debug/browser/breakpointsView.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 7b8df83d52e..bba2df5f6c2 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -260,9 +260,13 @@ export class BreakpointsView extends ViewPane { if (thread && thread.stoppedDetails && thread.stoppedDetails.hitBreakpointIds && thread.stoppedDetails.hitBreakpointIds.length > 0) { const hitBreakpointIds = thread.stoppedDetails.hitBreakpointIds; const elements = this.elements; - const index = elements.findIndex(e => e.getIdFromAdapter(thread.session.getId()) === hitBreakpointIds[0]); + const index = elements.findIndex(e => { + const id = e.getIdFromAdapter(thread.session.getId()); + return typeof id === 'number' && hitBreakpointIds.indexOf(id) !== -1; + }); if (index >= 0) { this.list.setFocus([index]); + this.list.setSelection([index]); } } } From b3c56f52813d26623e56199266d79675fbf1657f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 14 May 2021 11:26:56 -0700 Subject: [PATCH 047/270] testing: bad rendering of inline decorations for markdown messages --- src/vs/base/browser/markdownRenderer.ts | 8 ++++++++ .../contrib/testing/browser/testingDecorations.ts | 3 ++- .../contrib/testing/browser/testingOutputPeek.ts | 13 ++++--------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 5378627e57a..67fae43f7ab 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -312,6 +312,14 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti }; } +/** + * Strips all markdown from `string`, if it's an IMarkdownString. For example + * `# Header` would be output as `Header`. If it's not, the string is returned. + */ +export function renderStringAsPlaintext(string: IMarkdownString | string) { + return typeof string === 'string' ? string : renderMarkdownAsPlaintext(string); +} + /** * Strips all markdown from `markdown`. For example `# Header` would be output as `Header`. */ diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index aaafab933c9..9d98c778a2e 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; @@ -393,7 +394,7 @@ class TestMessageDecoration implements ITestDecoration { const colorTheme = themeService.getColorTheme(); editorService.registerDecorationType(this.decorationId, { after: { - contentText: message.toString(), + contentText: renderStringAsPlaintext(message), color: `${colorTheme.getColor(testMessageSeverityColors[severity].decorationForeground)}`, fontSize: `${editor.getOption(EditorOption.fontSize)}px`, fontFamily: `var(${FONT_FAMILY_VAR})`, diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 03b6cadad54..9b930df9f76 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; @@ -249,7 +248,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo this.peek.value!.create(); } - alert(toPlainText(message.message)); + alert(renderStringAsPlaintext(message.message)); this.peek.value!.setModel(dto); this.currentPeekUri = uri; } @@ -416,7 +415,7 @@ class TestingDiffOutputPeek extends TestingOutputPeek { this.test = test; this.show(message.location.range, hintDiffPeekHeight(message)); - this.setTitle(firstLine(toPlainText(message.message)), test.label); + this.setTitle(firstLine(renderStringAsPlaintext(message.message)), test.label); const [original, modified] = await Promise.all([ this.modelService.createModelReference(expectedUri), @@ -488,7 +487,7 @@ class TestingMessageOutputPeek extends TestingOutputPeek { this.test = test; - const messageStr = toPlainText(message.message); + const messageStr = renderStringAsPlaintext(message.message); this.show(message.location.range, hintPeekStrHeight(messageStr)); this.setTitle(firstLine(messageStr), test.label); @@ -526,10 +525,6 @@ class TestingMessageOutputPeek extends TestingOutputPeek { const hintDiffPeekHeight = (message: ITestMessage) => Math.max(hintPeekStrHeight(message.actualOutput), hintPeekStrHeight(message.expectedOutput)); -const toPlainText = (message: IMarkdownString | string) => typeof message === 'string' - ? message - : renderMarkdownAsPlaintext(message); - const firstLine = (str: string) => { const index = str.indexOf('\n'); return index === -1 ? str : str.slice(0, index); From 84c23ab5bd74a04543d77e467fd0441edabd7adb Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 18 May 2021 11:27:54 -0700 Subject: [PATCH 048/270] debug: bump node-debug2 version Fixes https://github.com/microsoft/vscode-remotehub/issues/119 --- product.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product.json b/product.json index 1667a363aa1..03cff420b86 100644 --- a/product.json +++ b/product.json @@ -48,7 +48,7 @@ }, { "name": "ms-vscode.node-debug2", - "version": "1.42.6", + "version": "1.42.7", "repo": "https://github.com/microsoft/vscode-node-debug2", "metadata": { "id": "36d19e17-7569-4841-a001-947eb18602b2", From b84a8c4215af7a5d2d0ee2b4b555c7fe1f051eda Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 18 May 2021 11:30:26 -0700 Subject: [PATCH 049/270] Re-enable selectionNavigation in terminal tabs Fixes #124126 --- src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 34b1151d9f3..aa8f693b2b5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -61,7 +61,7 @@ export class TerminalTabList extends WorkbenchList { @IInstantiationService instantiationService: IInstantiationService, @IDecorationsService _decorationsService: IDecorationsService ) { - super('TerminalTabsTree', container, + super('TerminalTabsList', container, { getHeight: () => TerminalTabsListSizes.TabHeight, getTemplateId: () => 'terminal.tabs' @@ -70,6 +70,7 @@ export class TerminalTabList extends WorkbenchList { { horizontalScrolling: false, supportDynamicHeights: false, + selectionNavigation: true, identityProvider: { getId: e => e?.instanceId }, From 25967171f17054fd062c2b12679263db55f6e4c6 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 18 May 2021 20:39:39 +0200 Subject: [PATCH 050/270] Delay workspace recommendations as remote extensions installations are ongoing For #124119 --- .../extensions/browser/extensionRecommendationsService.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 5477d2147cf..56aff6ead14 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -21,12 +21,15 @@ import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/k import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; +import { timeout } from 'vs/base/common/async'; type IgnoreRecommendationClassification = { recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; }; +const WORKSPACE_RECOMMENDATION_DELAY = 5000; + export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService { declare readonly _serviceBrand: undefined; @@ -238,10 +241,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); if (allowedRecommendations.length) { + await timeout(WORKSPACE_RECOMMENDATION_DELAY); // remote extensions might still being installed #124119 await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations); } } - - - } From e657d42281487040b50374d482d34b3fb05eb377 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 18 May 2021 11:56:09 -0700 Subject: [PATCH 051/270] debug: expose parent session on DebugSessions For https://github.com/microsoft/vscode/issues/123403 --- src/vs/vscode.proposed.d.ts | 12 ++++++++++++ .../workbench/api/browser/mainThreadDebugService.ts | 3 ++- src/vs/workbench/api/common/extHost.protocol.ts | 1 + src/vs/workbench/api/common/extHostDebugService.ts | 11 ++++++++--- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 64675d0840f..c620e195b73 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -3309,4 +3309,16 @@ declare module 'vscode' { } //#endregion + + //#region Expose parent session on DebugSessions - https://github.com/microsoft/vscode/issues/123403#issuecomment-843269200 + + export interface DebugSession { + /** + * The parent session of this debug session, if it was created as a child. + * @see DebugSessionOptions.parentSession + */ + readonly parentSession?: DebugSession; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 287bcd08305..60c3a74c017 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -329,7 +329,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb type: session.configuration.type, name: session.name, folderUri: session.root ? session.root.uri : undefined, - configuration: session.configuration + configuration: session.configuration, + parent: session.parentSession?.getId(), }; } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 42b5c7f9e95..aee0a700928 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1780,6 +1780,7 @@ export interface IDebugSessionFullDto { id: DebugSessionUUID; type: string; name: string; + parent: DebugSessionUUID | undefined; folderUri: UriComponents | undefined; configuration: IConfig; } diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 7dfe656ec43..b2fd2327d03 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -845,7 +845,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E let ds = this._debugSessions.get(dto.id); if (!ds) { const folder = await this.getFolder(dto.folderUri); - ds = new ExtHostDebugSession(this._debugServiceProxy, dto.id, dto.type, dto.name, folder, dto.configuration); + const parent = dto.parent ? this._debugSessions.get(dto.parent) : undefined; + ds = new ExtHostDebugSession(this._debugServiceProxy, dto.id, dto.type, dto.name, folder, dto.configuration, parent); this._debugSessions.set(ds.id, ds); this._debugServiceProxy.$sessionCached(ds.id); } @@ -872,7 +873,8 @@ export class ExtHostDebugSession implements vscode.DebugSession { private _type: string, private _name: string, private _workspaceFolder: vscode.WorkspaceFolder | undefined, - private _configuration: vscode.DebugConfiguration) { + private _configuration: vscode.DebugConfiguration, + private _parentSession: vscode.DebugSession | undefined) { } public get id(): string { @@ -886,12 +888,15 @@ export class ExtHostDebugSession implements vscode.DebugSession { public get name(): string { return this._name; } - public set name(name: string) { this._name = name; this._debugServiceProxy.$setDebugSessionName(this._id, name); } + public get parentSession(): vscode.DebugSession | undefined { + return this._parentSession; + } + _acceptNameChanged(name: string) { this._name = name; } From 73cce9a080e636d13f7f3f5c5d3b231497d0e9c0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 18 May 2021 11:58:40 -0700 Subject: [PATCH 052/270] Fix vertical terminal margin left/right Fixes #124101 --- src/vs/workbench/contrib/terminal/browser/media/terminal.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 00f31fc01a0..12ff01c047e 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -35,10 +35,10 @@ position: absolute; top: 0; } -.monaco-workbench .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:first-child .terminal-wrapper { +.monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .terminal-wrapper { margin-left: 20px; } -.monaco-workbench .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:last-child .terminal-wrapper { +.monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .terminal-wrapper { margin-right: 20px; } From 79bd573d9e4a56017b65b023947fcb07a1a6ac92 Mon Sep 17 00:00:00 2001 From: Rachel Macfarlane Date: Tue, 18 May 2021 12:46:42 -0700 Subject: [PATCH 053/270] Move OpenIssueReporter api command registration out of extHostApiCommands (#124068) --- src/vs/workbench/api/common/apiCommands.ts | 17 ------- .../api/common/extHostApiCommands.ts | 9 +--- .../issue/browser/issue.web.contribution.ts | 49 ++++++++++++++++++- .../contrib/issue/common/commands.ts | 2 + .../electron-sandbox/issue.contribution.ts | 46 ++++++++++++++++- 5 files changed, 96 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index da918ae31e8..e6a50589663 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -78,23 +78,6 @@ export class RemoveFromRecentlyOpenedAPICommand { } CommandsRegistry.registerCommand(RemoveFromRecentlyOpenedAPICommand.ID, adjustHandler(RemoveFromRecentlyOpenedAPICommand.execute)); -export interface OpenIssueReporterArgs { - readonly extensionId: string; - readonly issueTitle?: string; - readonly issueBody?: string; -} - -export class OpenIssueReporter { - public static readonly ID = 'vscode.openIssueReporter'; - - public static execute(executor: ICommandsExecutor, args: string | OpenIssueReporterArgs): Promise { - const commandArgs = typeof args === 'string' - ? { extensionId: args } - : args; - return executor.executeCommand('workbench.action.openIssueReporter', commandArgs); - } -} - interface RecentEntry { uri: URI; type: 'workspace' | 'folder' | 'file'; diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index d70f0a3f8da..844206bbd72 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -14,7 +14,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { CustomCodeAction } from 'vs/workbench/api/common/extHostLanguageFeatures'; -import { ICommandsExecutor, RemoveFromRecentlyOpenedAPICommand, OpenIssueReporter, OpenIssueReporterArgs } from './apiCommands'; +import { ICommandsExecutor, RemoveFromRecentlyOpenedAPICommand } from './apiCommands'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; @@ -456,13 +456,6 @@ export class ExtHostApiCommands { { name: 'path', description: 'Path to remove from recently opened.', constraint: (value: any) => typeof value === 'string' } ] }); - - this._register(OpenIssueReporter.ID, adjustHandler(OpenIssueReporter.execute), { - description: 'Opens the issue reporter with the provided extension id as the selected source', - args: [ - { name: 'extensionId', description: 'extensionId to report an issue on', constraint: (value: unknown) => typeof value === 'string' || (typeof value === 'object' && typeof (value as OpenIssueReporterArgs).extensionId === 'string') } - ] - }); } // --- command impl diff --git a/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts b/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts index 190c892525f..51965b5220c 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts @@ -13,7 +13,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IWebIssueService, WebIssueService } from 'vs/workbench/contrib/issue/browser/issueService'; -import { OpenIssueReporterArgs, OpenIssueReporterActionId } from 'vs/workbench/contrib/issue/common/commands'; +import { OpenIssueReporterArgs, OpenIssueReporterActionId, OpenIssueReporterApiCommandId } from 'vs/workbench/contrib/issue/common/commands'; class RegisterIssueContribution implements IWorkbenchContribution { @@ -34,6 +34,53 @@ class RegisterIssueContribution implements IWorkbenchContribution { return accessor.get(IWebIssueService).openReporter({ extensionId }); }); + CommandsRegistry.registerCommand({ + id: OpenIssueReporterApiCommandId, + handler: function (accessor, args?: [string] | OpenIssueReporterArgs) { + let extensionId: string | undefined; + if (args) { + if (Array.isArray(args)) { + [extensionId] = args; + } else { + extensionId = args.extensionId; + } + } + + if (!!extensionId && typeof extensionId !== 'string') { + throw new Error(`Invalid argument when running '${OpenIssueReporterApiCommandId}: 'extensionId' must be of type string `); + } + + return accessor.get(IWebIssueService).openReporter({ extensionId }); + }, + description: { + description: 'Open the issue reporter and optionally prefill part of the form.', + args: [ + { + name: 'options', + description: 'Data to use to prefill the issue reporter with.', + isOptional: true, + schema: { + oneOf: [ + { + type: 'string', + description: 'The extension id to preselect.' + }, + { + type: 'object', + properties: { + extensionId: { + type: 'string' + }, + } + + } + ] + } + }, + ] + } + }); + const command: ICommandAction = { id: OpenIssueReporterActionId, title: { value: OpenIssueReporterActionLabel, original: 'Report Issue' }, diff --git a/src/vs/workbench/contrib/issue/common/commands.ts b/src/vs/workbench/contrib/issue/common/commands.ts index bcd6e252c88..a5a9ff998f2 100644 --- a/src/vs/workbench/contrib/issue/common/commands.ts +++ b/src/vs/workbench/contrib/issue/common/commands.ts @@ -5,6 +5,8 @@ export const OpenIssueReporterActionId = 'workbench.action.openIssueReporter'; +export const OpenIssueReporterApiCommandId = 'vscode.openIssueReporter'; + export interface OpenIssueReporterArgs { readonly extensionId?: string; readonly issueTitle?: string; diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 84177acd367..c7f49c38f05 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -14,7 +14,7 @@ import { WorkbenchIssueService } from 'vs/workbench/services/issue/electron-sand import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IssueReporterData } from 'vs/platform/issue/common/issue'; import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; -import { OpenIssueReporterArgs, OpenIssueReporterActionId } from 'vs/workbench/contrib/issue/common/commands'; +import { OpenIssueReporterArgs, OpenIssueReporterActionId, OpenIssueReporterApiCommandId } from 'vs/workbench/contrib/issue/common/commands'; if (!!product.reportIssueUrl) { registerAction2(ReportPerformanceIssueUsingReporterAction); @@ -27,6 +27,50 @@ if (!!product.reportIssueUrl) { return accessor.get(IWorkbenchIssueService).openReporter(data); }); + CommandsRegistry.registerCommand({ + id: OpenIssueReporterApiCommandId, + handler: function (accessor, args?: [string] | OpenIssueReporterArgs) { + const data: Partial = Array.isArray(args) + ? { extensionId: args[0] } + : args || {}; + + return accessor.get(IWorkbenchIssueService).openReporter(data); + }, + description: { + description: 'Open the issue reporter and optionally prefill part of the form.', + args: [ + { + name: 'options', + description: 'Data to use to prefill the issue reporter with.', + isOptional: true, + schema: { + oneOf: [ + { + type: 'string', + description: 'The extension id to preselect.' + }, + { + type: 'object', + properties: { + extensionId: { + type: 'string' + }, + issueTitle: { + type: 'string' + }, + issueBody: { + type: 'string' + } + } + + } + ] + } + }, + ] + } + }); + const reportIssue: ICommandAction = { id: OpenIssueReporterActionId, title: { From 25873bb34c46868445c931a2c7d120cf105bba04 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Tue, 18 May 2021 16:23:18 -0400 Subject: [PATCH 054/270] Adds Uri.from #121198 --- src/vs/vscode.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 0e680b8175f..40cf4af251f 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1314,6 +1314,15 @@ declare module 'vscode' { */ static joinPath(base: Uri, ...pathSegments: string[]): Uri; + /** + * Create an URI from its component parts + * + * @see {@link Uri.toString} + * @param components The component parts of an Uri. + * @return A new Uri instance. + */ + static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri; + /** * Use the `file` and `parse` factory functions to create new `Uri` objects. */ From 8b726fc9f51a7384e6a2e1aca68a433cc2e49a3c Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Tue, 18 May 2021 15:56:37 -0500 Subject: [PATCH 055/270] update color registry names for inline values --- .../contrib/debug/browser/debugEditorContribution.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 84112893d02..4f58d0457bf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -54,17 +54,17 @@ const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped -export const debugInlineForeground = registerColor('debugInline.decorationForeground', { +export const debugInlineForeground = registerColor('editor.inlineValuesForeground', { dark: '#ffffff80', light: '#00000080', hc: '#ffffff80' -}, nls.localize('debugInline.decorationForeground', "Color for the debug inline value text.")); +}, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text.")); -export const debugInlineBackground = registerColor('debugInline.decorationBackground', { +export const debugInlineBackground = registerColor('editor.inlineValuesBackground', { dark: '#ffc80033', light: '#ffc80033', hc: '#ffc80033' -}, nls.localize('debugInline.decorationBackground', "Color for the debug inline value background.")); +}, nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); class InlineSegment { constructor(public column: number, public text: string) { From 9b1ba3f31f02a23ad1acc605a59bfef0f84a65c5 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Tue, 18 May 2021 16:38:27 -0400 Subject: [PATCH 056/270] Adds Memento.keys #87110 --- src/vs/vscode.proposed.d.ts | 12 ++++++++++++ src/vs/workbench/api/common/extHostMemento.ts | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index c620e195b73..0d402ba61e0 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -3321,4 +3321,16 @@ declare module 'vscode' { } //#endregion + + //#region https://github.com/microsoft/vscode/issues/87110 @eamodio + + export interface Memento { + + /** + * The stored keys. + */ + readonly keys: readonly string[]; + } + + //#endregion } diff --git a/src/vs/workbench/api/common/extHostMemento.ts b/src/vs/workbench/api/common/extHostMemento.ts index 31c1676e696..e4f029c0b12 100644 --- a/src/vs/workbench/api/common/extHostMemento.ts +++ b/src/vs/workbench/api/common/extHostMemento.ts @@ -56,6 +56,11 @@ export class ExtensionMemento implements vscode.Memento { }, 0); } + get keys(): readonly string[] { + // Filter out `undefined` values, as they can stick around in the `_value` until the `onDidChangeStorage` event runs + return Object.entries(this._value ?? {}).filter(([, value]) => value !== undefined).map(([key]) => key); + } + get whenReady(): Promise { return this._init; } From 9063312f9c1eb77232c8307f1b34de906cc3ed59 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 May 2021 08:54:28 -0700 Subject: [PATCH 057/270] keep ref from cellVM to outputVM for now. --- .../contrib/notebook/browser/viewModel/codeCellViewModel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 3049c90f3b3..c757fcc2671 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -411,6 +411,5 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this._outputCollection = []; this._outputsTop = null; - this._outputViewModels = []; } } From bf9f6837d14373512fdd8f97aba16e9152a15631 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 May 2021 10:44:10 -0700 Subject: [PATCH 058/270] NotebooKernelActionViewItem accepts notebook editor widget. --- .../notebook/browser/notebookKernelActionViewItem.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts index b473b71f80d..9a70372f350 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem.ts @@ -12,6 +12,7 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd import { selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { INotebookKernelMatchResult, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; registerThemingParticipant((theme, collector) => { const value = theme.getColor(toolbarHoverBackground); @@ -26,7 +27,7 @@ export class NotebooKernelActionViewItem extends ActionViewItem { constructor( actualAction: IAction, - private readonly _editor: NotebookEditor, + private readonly _editor: NotebookEditor | INotebookEditor, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, ) { super( @@ -56,13 +57,14 @@ export class NotebooKernelActionViewItem extends ActionViewItem { } } - private _update(): void { - const widget = this._editor.getControl(); - if (!widget || !widget.hasModel()) { + protected _update(): void { + const notebook = this._editor.viewModel?.notebookDocument; + + if (!notebook) { this._resetAction(); return; } - const notebook = widget.viewModel.notebookDocument; + const info = this._notebookKernelService.getMatchingKernel(notebook); this._updateActionFromKernelInfo(info); } From d501abe922d143fce79b0d7b59b5b7ed3b9e5c4e Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 May 2021 10:46:01 -0700 Subject: [PATCH 059/270] contribute actions to the toolbar. --- src/vs/platform/actions/common/actions.ts | 1 + .../api/common/menusExtensionPoint.ts | 6 +++ .../notebook/browser/contrib/coreActions.ts | 39 +++++++++++++++++-- .../browser/contrib/status/editorStatus.ts | 21 ++++++++-- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index abe86723b10..12be93a4201 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -143,6 +143,7 @@ export class MenuId { static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); static readonly NotebookToolbar = new MenuId('NotebookToolbar'); + static readonly NotebookRightToolbar = new MenuId('NotebookRightToolbar'); static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); static readonly NotebookCellInsert = new MenuId('NotebookCellInsert'); static readonly NotebookCellBetween = new MenuId('NotebookCellBetween'); diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 3046d981ff3..7b3a989c920 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -182,6 +182,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('notebook.toolbar', "The contributed notebook toolbar menu"), proposed: true }, + { + key: 'notebook/toolbar/right', + id: MenuId.NotebookRightToolbar, + description: localize('notebook.toolbar.right', "The contributed notebook right toolbar menu"), + proposed: true + }, { key: 'notebook/cell/title', id: MenuId.NotebookCellTitle, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 059c703ab37..f8e5427ebdc 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -34,6 +34,7 @@ import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { Iterable } from 'vs/base/common/iterator'; import { flatten } from 'vs/base/common/arrays'; +import { Codicon } from 'vs/base/common/codicons'; // Kernel Command export const SELECT_KERNEL_ID = 'notebook.selectKernel'; @@ -684,7 +685,7 @@ registerAction2(class ExecuteNotebookAction extends NotebookAction { { id: MenuId.NotebookToolbar, order: -1, - group: 'navigation', + group: 'navigation/execute', when: ContextKeyExpr.and( executeNotebookCondition, ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_RUNNING_CELL.toNegated()), @@ -755,7 +756,7 @@ registerAction2(class CancelNotebook extends NotebookAction { { id: MenuId.NotebookToolbar, order: -1, - group: 'navigation', + group: 'navigation/execute', when: ContextKeyExpr.and( NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, @@ -916,7 +917,13 @@ abstract class InsertCellCommand extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { - context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, true); + if (context.cell) { + context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, true); + } else { + const focusRange = context.notebookEditor.getFocus(); + const next = focusRange.end - 1; + context.notebookEditor.insertNotebookCell(context.notebookEditor.viewModel.viewCells[next], this.kind, this.direction, undefined, true); + } } } @@ -1023,6 +1030,18 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) }); +MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { + command: { + id: INSERT_CODE_CELL_BELOW_COMMAND_ID, + icon: Codicon.add, + title: localize('notebookActions.menu.insertCode.ontoolbar', "Code"), + tooltip: localize('notebookActions.menu.insertCode.tooltip', "Add Code Cell") + }, + order: -5, + group: 'navigation/add', + when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) +}); + MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { command: { id: INSERT_CODE_CELL_AT_TOP_COMMAND_ID, @@ -1077,6 +1096,18 @@ MenuRegistry.appendMenuItem(MenuId.NotebookCellBetween, { when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) }); +MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { + command: { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + icon: Codicon.add, + title: localize('notebookActions.menu.insertMarkdown.ontoolbar', "Markdown"), + tooltip: localize('notebookActions.menu.insertMarkdown.tooltip', "Add Markdown Cell") + }, + order: -5, + group: 'navigation/add', + when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) +}); + MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { command: { id: INSERT_MARKDOWN_CELL_AT_TOP_COMMAND_ID, @@ -1503,7 +1534,7 @@ registerAction2(class ClearAllCellOutputsAction extends NotebookAction { { id: MenuId.NotebookToolbar, when: ContextKeyExpr.equals('config.notebook.experimental.globalToolbar', true), - group: 'navigation', + group: 'navigation/execute', order: 0 } ], diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 3cd8440458b..2e2ce57d5eb 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -37,12 +37,27 @@ registerAction2(class extends Action2 { precondition: NOTEBOOK_IS_ACTIVE_EDITOR, icon: selectKernelIcon, f1: true, - menu: { + menu: [{ id: MenuId.EditorTitle, - when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), ContextKeyExpr.equals('config.notebook.experimental.showKernelInEditorTitle', true),), + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), + ContextKeyExpr.equals('config.notebook.experimental.showKernelInEditorTitle', true), + ContextKeyExpr.notEquals('config.notebook.experimental.globalToolbar', true) + ), group: 'navigation', order: -10 - }, + }, { + id: MenuId.NotebookToolbar, + when: ContextKeyExpr.and( + NOTEBOOK_IS_ACTIVE_EDITOR, + NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), + ContextKeyExpr.equals('config.notebook.experimental.showKernelInEditorTitle', true), + ContextKeyExpr.equals('config.notebook.experimental.globalToolbar', true) + ), + group: 'status', + order: -10 + }], description: { description: nls.localize('notebookActions.selectKernel.args', "Notebook Kernel Args"), args: [ From 4486788c81c90ba6d4842416f2f16558fba04ad6 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 May 2021 14:30:21 -0700 Subject: [PATCH 060/270] allow the left toolbar to overflow. --- .../browser/contrib/status/editorStatus.ts | 1 - .../notebook/browser/media/notebook.css | 47 ++++- .../notebook/browser/notebookEditorToolbar.ts | 196 ++++++++++++++++++ .../notebook/browser/notebookEditorWidget.ts | 112 +--------- .../browser/view/renderers/cellMenus.ts | 4 + 5 files changed, 250 insertions(+), 110 deletions(-) create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 2e2ce57d5eb..f06c862be27 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -50,7 +50,6 @@ registerAction2(class extends Action2 { }, { id: MenuId.NotebookToolbar, when: ContextKeyExpr.and( - NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), ContextKeyExpr.equals('config.notebook.experimental.showKernelInEditorTitle', true), ContextKeyExpr.equals('config.notebook.experimental.globalToolbar', true) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 489ef010508..5b306dcdb18 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -11,15 +11,14 @@ position: relative; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar { +.monaco-workbench .notebookOverlay .notebook-toolbar-container { width: 100%; - display: inline-flex; - padding-left: 8px; + display: flex; margin-top: 2px; margin-bottom: 2px; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar .monaco-action-bar .action-item { +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item { height: 22px; display: flex; align-items: center; @@ -27,21 +26,53 @@ margin-right: 8px; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar .monaco-action-bar .action-item:hover { +.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element { + flex: 1; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element .notebook-toolbar-left { + padding: 0px 8px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-left { + width: fit-content; + display: flex; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-right { + display: flex; + padding: 0px 0px 0px 8px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .kernel-label { + background-size: 16px; + padding: 0px 5px 0px 3px; + border-radius: 5px; + font-size: 13px; + height: 22px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-left .monaco-action-bar .action-item .action-label.separator { + margin: 5px 0px !important; + padding: 0px !important; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item:hover { background-color: var(--code-toolbarHoverBackground); } -.monaco-workbench .notebookOverlay .notebook-top-toolbar .monaco-action-bar .action-item .action-label { +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .action-label { background-size: 16px; + padding-left: 2px; } .monaco-workbench .notebook-action-view-item .action-label { display: inline-flex; } -.monaco-workbench .notebookOverlay .notebook-top-toolbar .monaco-action-bar .action-item .notebook-label { +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .notebook-label { background-size: 16px; - padding: 0px 5px 0px 3px; + padding: 0px 5px 0px 2px; border-radius: 5px; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts new file mode 100644 index 00000000000..7d91627f4a8 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { INotebookEditor, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem'; +import { ActionViewWithLabel } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; + +export class NotebookEditorToolbar extends Disposable { + // private _editorToolbarContainer!: HTMLElement; + private _leftToolbarScrollable!: DomScrollableElement; + private _notebookTopLeftToolbarContainer!: HTMLElement; + private _notebookTopRightToolbarContainer!: HTMLElement; + private _notebookGlobalActionsMenu!: IMenu; + private _notebookLeftToolbar!: ToolBar; + private _notebookRightToolbar!: ToolBar; + private _useGlobalToolbar: boolean = false; + + private readonly _onDidChangeState = this._register(new Emitter()); + onDidChangeState: Event = this._onDidChangeState.event; + + get useGlobalToolbar(): boolean { + return this._useGlobalToolbar; + } + + constructor( + readonly notebookEditor: INotebookEditor, + readonly contextKeyService: IContextKeyService, + readonly domNode: HTMLElement, + @IInstantiationService readonly instantiationService: IInstantiationService, + @IConfigurationService readonly configurationService: IConfigurationService, + @IContextMenuService readonly contextMenuService: IContextMenuService, + @IEditorService private readonly editorService: IEditorService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService + ) { + super(); + + this._buildBody(); + + this._register(this.editorService.onDidActiveEditorChange(() => { + if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { + const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; + if (notebookEditor === this.notebookEditor) { + // this is the active editor + this._showNotebookActionsinEditorToolbar(); + return; + } + } + })); + + this._reigsterNotebookActionsToolbar(); + } + + private _buildBody() { + this._notebookTopLeftToolbarContainer = document.createElement('div'); + this._notebookTopLeftToolbarContainer.classList.add('notebook-toolbar-left'); + this._leftToolbarScrollable = new DomScrollableElement(this._notebookTopLeftToolbarContainer, { + vertical: ScrollbarVisibility.Hidden, + horizontal: ScrollbarVisibility.Visible, + horizontalScrollbarSize: 3, + useShadows: false, + scrollYToX: true + }); + this._register(this._leftToolbarScrollable); + + DOM.append(this.domNode, this._leftToolbarScrollable.getDomNode()); + this._notebookTopRightToolbarContainer = document.createElement('div'); + this._notebookTopRightToolbarContainer.classList.add('notebook-toolbar-right'); + DOM.append(this.domNode, this._notebookTopRightToolbarContainer); + } + + private _reigsterNotebookActionsToolbar() { + const cellMenu = this.instantiationService.createInstance(CellMenus); + this._notebookGlobalActionsMenu = this._register(cellMenu.getNotebookToolbar(this.contextKeyService)); + this._register(this._notebookGlobalActionsMenu); + + this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar') ?? false; + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('notebook.experimental.globalToolbar')) { + this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar'); + this._showNotebookActionsinEditorToolbar(); + } + })); + + const context = { + ui: true, + notebookEditor: this + }; + + const actionProvider = (action: IAction) => { + if (action.id === SELECT_KERNEL_ID) { + // // this is being disposed by the consumer + return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor); + } + + return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action) : undefined; + }; + + this._notebookLeftToolbar = new ToolBar(this._notebookTopLeftToolbarContainer, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: actionProvider, + renderDropdownAsChildElement: true + }); + this._register(this._notebookLeftToolbar); + this._notebookLeftToolbar.context = context; + + this._notebookRightToolbar = new ToolBar(this._notebookTopRightToolbarContainer, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: actionProvider, + renderDropdownAsChildElement: true + }); + this._register(this._notebookRightToolbar); + this._notebookRightToolbar.context = context; + + this._showNotebookActionsinEditorToolbar(); + this._register(this._notebookGlobalActionsMenu.onDidChange(() => { + this._showNotebookActionsinEditorToolbar(); + })); + + if (this.experimentService) { + this.experimentService.getTreatment('nbtoolbarineditor').then(treatment => { + if (treatment === undefined) { + return; + } + if (this._useGlobalToolbar !== treatment) { + this._useGlobalToolbar = treatment; + this._showNotebookActionsinEditorToolbar(); + } + }); + } + } + + private _showNotebookActionsinEditorToolbar() { + // when there is no view model, just ignore. + if (!this.notebookEditor.hasModel()) { + return; + } + + if (!this._useGlobalToolbar) { + this.domNode.style.display = 'none'; + } else { + this._notebookLeftToolbar.setActions([], []); + const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true }); + this.domNode.style.display = 'flex'; + const primaryLeftGroups = groups.filter(group => /^navigation/.test(group[0])); + let primaryActions: IAction[] = []; + primaryLeftGroups.sort((a, b) => { + if (a[0] === 'navigation') { + return 1; + } + + if (b[0] === 'navigation') { + return -1; + } + + return 0; + }).forEach((group, index) => { + primaryActions.push(...group[1]); + if (index < primaryLeftGroups.length - 1) { + primaryActions.push(new Separator()); + } + }); + const primaryRightGroup = groups.find(group => /^status/.test(group[0])); + const primaryRightActions = primaryRightGroup ? primaryRightGroup[1] : []; + const secondaryActions = groups.filter(group => /^navigation/.test(group[0]) && /^status/.test(group[0])).reduce((prev: (MenuItemAction | SubmenuItemAction)[], curr) => { prev.push(...curr[1]); return prev; }, []); + + this._notebookLeftToolbar.setActions(primaryActions, secondaryActions); + this._notebookRightToolbar.setActions(primaryRightActions, []); + } + + this._onDidChangeState.fire(); + + // if (this._dimension && this._isVisible) { + // this.layout(this._dimension); + // } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index bf5eb6e6228..15c7211e4f9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -26,11 +26,11 @@ import { IEditor } from 'vs/editor/common/editorCommon'; import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -64,11 +64,6 @@ import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; -import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { isWeb } from 'vs/base/common/platform'; import { mark } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; import { readFontInfo } from 'vs/editor/browser/config/configuration'; @@ -77,7 +72,7 @@ import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; -import { ActionViewWithLabel } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; +import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/notebookEditorToolbar'; const $ = DOM.$; @@ -205,6 +200,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private static readonly EDITOR_MEMENTOS = new Map>(); private _overlayContainer!: HTMLElement; private _notebookTopToolbarContainer!: HTMLElement; + private _notebookTopToolbar!: NotebookEditorToolbar; private _body!: HTMLElement; private _styleElement!: HTMLStyleElement; private _overflowContainer!: HTMLElement; @@ -323,7 +319,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IAccessibilityService accessibilityService: IAccessibilityService, @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, - @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @ILayoutService private readonly layoutService: ILayoutService, @@ -331,9 +326,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IMenuService private readonly menuService: IMenuService, @IThemeService private readonly themeService: IThemeService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IModeService private readonly modeService: IModeService, - @IKeybindingService private readonly keybindingService: IKeybindingService, - @optional(ITASExperimentService) private readonly experimentService: ITASExperimentService + @IModeService private readonly modeService: IModeService ) { super(); this.isEmbedded = creationOptions.isEmbedded || false; @@ -535,7 +528,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _createBody(parent: HTMLElement): void { this._notebookTopToolbarContainer = document.createElement('div'); - this._notebookTopToolbarContainer.classList.add('notebook-top-toolbar'); + this._notebookTopToolbarContainer.classList.add('notebook-toolbar-container'); this._notebookTopToolbarContainer.style.display = 'none'; DOM.append(parent, this._notebookTopToolbarContainer); this._body = document.createElement('div'); @@ -770,18 +763,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._register(widgetFocusTracker.onDidBlur(() => this._onDidBlurEmitter.fire())); this._reigsterNotebookActionsToolbar(); - this._register(this.editorService.onDidActiveEditorChange(() => { - if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditor; - if (notebookEditor === this) { - // this is the active editor - this._showNotebookActionsinEditorToolbar(); - return; - } - } - this._toolbarActionDisposable.clear(); - })); } private showListContextMenu(e: IListContextMenuEvent) { @@ -797,81 +779,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }); } - private _notebookGlobalActionsMenu!: IMenu; - private _toolbarActionDisposable = this._register(new DisposableStore()); - private _topToolbar!: ToolBar; - private _useGlobalToolbar: boolean = false; private _reigsterNotebookActionsToolbar() { - const cellMenu = this.instantiationService.createInstance(CellMenus); - this._notebookGlobalActionsMenu = this._register(cellMenu.getNotebookToolbar(this.scopedContextKeyService)); - this._register(this._notebookGlobalActionsMenu); - - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar') ?? false; - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('notebook.experimental.globalToolbar')) { - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar'); - this._showNotebookActionsinEditorToolbar(); + this._notebookTopToolbar = this._register(this.instantiationService.createInstance(NotebookEditorToolbar, this, this.scopedContextKeyService, this._notebookTopToolbarContainer)); + this._register(this._notebookTopToolbar.onDidChangeState(() => { + if (this._dimension && this._isVisible) { + this.layout(this._dimension); } })); - - this._topToolbar = new ToolBar(this._notebookTopToolbarContainer, this.contextMenuService, { - getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - actionViewItemProvider: action => { - if (action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ActionViewWithLabel, action); - } else { - return undefined; - } - }, - renderDropdownAsChildElement: true - }); - this._register(this._topToolbar); - this._topToolbar.context = { - ui: true, - notebookEditor: this - }; - - this._showNotebookActionsinEditorToolbar(); - this._register(this._notebookGlobalActionsMenu.onDidChange(() => { - this._showNotebookActionsinEditorToolbar(); - })); - - if (this.experimentService) { - this.experimentService.getTreatment('nbtoolbarineditor').then(treatment => { - if (treatment === undefined) { - return; - } - if (this._useGlobalToolbar !== treatment) { - this._useGlobalToolbar = treatment; - this._showNotebookActionsinEditorToolbar(); - } - }); - } - } - - private _showNotebookActionsinEditorToolbar() { - // when there is no view model, just ignore. - if (!this.viewModel) { - return; - } - - if (!this._useGlobalToolbar) { - this._notebookTopToolbarContainer.style.display = 'none'; - } else { - this._toolbarActionDisposable.clear(); - this._topToolbar.setActions([], []); - const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true }); - this._notebookTopToolbarContainer.style.display = 'flex'; - const primaryGroup = groups.find(group => group[0] === 'navigation'); - const primaryActions = primaryGroup ? primaryGroup[1] : []; - const secondaryActions = groups.filter(group => group[0] !== 'navigation').reduce((prev: (MenuItemAction | SubmenuItemAction)[], curr) => { prev.push(...curr[1]); return prev; }, []); - - this._topToolbar.setActions(primaryActions, secondaryActions); - } - - if (this._dimension && this._isVisible) { - this.layout(this._dimension); - } } private _updateForCursorNavigationMode(applyFocusChange: () => void): void { @@ -1440,7 +1354,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } this._dimension = new DOM.Dimension(dimension.width, dimension.height); - DOM.size(this._body, dimension.width, dimension.height - (this._useGlobalToolbar ? /** Toolbar height */ 26 : 0)); + DOM.size(this._body, dimension.width, dimension.height - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0)); if (this._list.getRenderHeight() < dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP) { // the new dimension is larger than the list viewport, update its additional height first, otherwise the list view will move down a bit (as the `scrollBottom` will move down) this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); @@ -1509,10 +1423,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const focused = DOM.isAncestor(document.activeElement, this._overlayContainer); this._editorFocus.set(focused); this.viewModel?.setFocus(focused); - - if (!focused) { - this._toolbarActionDisposable.clear(); - } } hasFocus() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts index 7b4e7e7204d..54febbfebe3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -16,6 +16,10 @@ export class CellMenus { return this.getMenu(MenuId.NotebookToolbar, contextKeyService); } + getNotebookRightToolbar(contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.NotebookRightToolbar, contextKeyService); + } + getCellTitleMenu(contextKeyService: IContextKeyService): IMenu { return this.getMenu(MenuId.NotebookCellTitle, contextKeyService); } From 2a9f02348f658c445571a207615d15910ac42214 Mon Sep 17 00:00:00 2001 From: Miguel Solorio Date: Tue, 18 May 2021 14:55:05 -0700 Subject: [PATCH 061/270] Make notebook toolbar cell visible on click (change notebook.cellToolbarVisibility default) --- .../workbench/contrib/notebook/browser/notebook.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 1ac18f58be5..7142116ab99 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -591,7 +591,7 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('notebook.cellToolbarVisibility.description', "Whether the cell toolbar should appear on hover or click."), type: 'string', enum: ['hover', 'click'], - default: 'hover' + default: 'click' }, } }); From 0026416a7eab32bf0dd03ad7aaa7da1a9dd8198a Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Tue, 18 May 2021 16:55:35 -0500 Subject: [PATCH 062/270] move showDeprecated suggest to be a CompletionOption option --- src/vs/editor/contrib/suggest/completionModel.ts | 5 +---- src/vs/editor/contrib/suggest/suggest.ts | 5 +++++ src/vs/editor/contrib/suggest/suggestModel.ts | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/suggest/completionModel.ts b/src/vs/editor/contrib/suggest/completionModel.ts index 26517ad8b48..40ee6005eca 100644 --- a/src/vs/editor/contrib/suggest/completionModel.ts +++ b/src/vs/editor/contrib/suggest/completionModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer, FuzzyScore, anyScore } from 'vs/base/common/filters'; -import { CompletionItemProvider, CompletionItemKind, CompletionItemTag } from 'vs/editor/common/modes'; +import { CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes'; import { CompletionItem } from './suggest'; import { InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; import { WordDistance } from 'vs/editor/contrib/suggest/wordDistance'; @@ -160,9 +160,6 @@ export class CompletionModel { if (item.isInvalid) { continue; // SKIP invalid items } - if (!this._options.showDeprecated && item.completion.tags?.includes(CompletionItemTag.Deprecated)) { - continue; // skip deprecated items - } // collect all support, know if their result is incomplete this._providerInfo.set(item.provider, Boolean(item.container.incomplete)); diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index c8e46227448..ec192ae88cf 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -155,6 +155,7 @@ export class CompletionOptions { readonly snippetSortOrder = SnippetSortOrder.Bottom, readonly kindFilter = new Set(), readonly providerFilter = new Set(), + readonly showDeprecated = true ) { } } @@ -216,6 +217,10 @@ export async function provideSuggestionItems( } for (let suggestion of container.suggestions) { if (!options.kindFilter.has(suggestion.kind)) { + // skip if not showing deprecated suggestions + if (!options.showDeprecated && suggestion?.tags?.includes(modes.CompletionItemTag.Deprecated)) { + continue; + } // fill in default range when missing if (!suggestion.range) { suggestion.range = defaultRange; diff --git a/src/vs/editor/contrib/suggest/suggestModel.ts b/src/vs/editor/contrib/suggest/suggestModel.ts index 747f78a4b4c..1880ef1bd9d 100644 --- a/src/vs/editor/contrib/suggest/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/suggestModel.ts @@ -428,13 +428,13 @@ export class SuggestModel implements IDisposable { break; } - const itemKindFilter = SuggestModel._createItemKindFilter(this._editor); + const { itemKind: itemKindFilter, showDeprecated } = SuggestModel._createSuggestFilter(this._editor); const wordDistance = WordDistance.create(this._editorWorkerService, this._editor); const completions = provideSuggestionItems( model, this._editor.getPosition(), - new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom), + new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom, showDeprecated), suggestCtx, this._requestToken.token ); @@ -502,7 +502,7 @@ export class SuggestModel implements IDisposable { }); } - private static _createItemKindFilter(editor: ICodeEditor): Set { + private static _createSuggestFilter(editor: ICodeEditor): { itemKind: Set; showDeprecated: boolean } { // kind filter and snippet sort rules const result = new Set(); @@ -543,7 +543,7 @@ export class SuggestModel implements IDisposable { if (!suggestOptions.showUsers) { result.add(CompletionItemKind.User); } if (!suggestOptions.showIssues) { result.add(CompletionItemKind.Issue); } - return result; + return { itemKind: result, showDeprecated: suggestOptions.showDeprecated }; } private _onNewContext(ctx: LineContext): void { From a069768c9b63cef827304884306382ca28c8f599 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Tue, 18 May 2021 19:40:21 -0400 Subject: [PATCH 063/270] Remove disposing of editor input in override service --- .../services/editor/browser/editorOverrideService.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index bcb8ad48584..5ca97d5b24c 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -132,10 +132,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride // Wait one second to give the user ample time to see the current editor then ask them to configure a default this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); } - // Dispose of the passed in editor as we will return a new one - if (!input?.editor.matches(editor)) { - editor.dispose(); - } + // Add the group as we might've changed it with the quickpick if (input) { this.sendOverrideTelemetry(input.editor); From 4815c9e99547125124e9c4856812645a23cafe59 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 May 2021 16:51:17 -0700 Subject: [PATCH 064/270] fix notebook toolbar scrollbar --- .../notebook/browser/media/notebook.css | 5 --- .../notebook/browser/notebookEditorToolbar.ts | 32 +++++++++++++++---- .../notebook/browser/notebookEditorWidget.ts | 2 ++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 5b306dcdb18..364c9c27432 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -34,11 +34,6 @@ padding: 0px 8px; } -.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-left { - width: fit-content; - display: flex; -} - .monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-right { display: flex; padding: 0px 0px 0px 8px; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts index 7d91627f4a8..ecddc6d68cd 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts @@ -8,7 +8,7 @@ import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableEle import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction, Separator } from 'vs/base/common/actions'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -41,6 +41,8 @@ export class NotebookEditorToolbar extends Disposable { return this._useGlobalToolbar; } + private _pendingLayout: IDisposable | undefined; + constructor( readonly notebookEditor: INotebookEditor, readonly contextKeyService: IContextKeyService, @@ -75,7 +77,7 @@ export class NotebookEditorToolbar extends Disposable { this._notebookTopLeftToolbarContainer.classList.add('notebook-toolbar-left'); this._leftToolbarScrollable = new DomScrollableElement(this._notebookTopLeftToolbarContainer, { vertical: ScrollbarVisibility.Hidden, - horizontal: ScrollbarVisibility.Visible, + horizontal: ScrollbarVisibility.Auto, horizontalScrollbarSize: 3, useShadows: false, scrollYToX: true @@ -103,7 +105,7 @@ export class NotebookEditorToolbar extends Disposable { const context = { ui: true, - notebookEditor: this + notebookEditor: this.notebookEditor }; const actionProvider = (action: IAction) => { @@ -185,12 +187,30 @@ export class NotebookEditorToolbar extends Disposable { this._notebookLeftToolbar.setActions(primaryActions, secondaryActions); this._notebookRightToolbar.setActions(primaryRightActions, []); + this._updateScrollbar(); } this._onDidChangeState.fire(); + } - // if (this._dimension && this._isVisible) { - // this.layout(this._dimension); - // } + layout() { + this._updateScrollbar(); + } + + private _updateScrollbar() { + this._pendingLayout?.dispose(); + + this._pendingLayout = DOM.measure(() => { + DOM.measure(() => { // double RAF + this._leftToolbarScrollable.setRevealOnScroll(false); + this._leftToolbarScrollable.scanDomNode(); + this._leftToolbarScrollable.setRevealOnScroll(true); + }); + }); + } + + override dispose() { + this._pendingLayout?.dispose(); + super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 15c7211e4f9..2307875a22b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1380,6 +1380,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webviewTransparentCover.style.width = `${dimension.width}px`; } + this._notebookTopToolbar.layout(); + this._viewContext?.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); } //#endregion From 616e0fd99270e5a59a0762849a5085268743de4c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 18 May 2021 16:59:09 -0700 Subject: [PATCH 065/270] refactor: polish - Fix mis-used dependencies. Add the roughly proposed 'extends' model for notebook renderers. Keep it out of contribution schema for now until we can work it more. - Made new loading work with JS modules. - Consolidated the 'old style' code in the renderer so that we can just delete it when the time comes. - Removed duplicated code and sharp edges from the 'queue' mechaism. --- .../notebook-markdown-extensions/package.json | 22 +- .../notebook/browser/extensionPoint.ts | 25 +- .../view/renderers/backLayerWebView.ts | 58 +-- .../browser/view/renderers/webviewPreloads.ts | 378 ++++++++++-------- .../contrib/notebook/common/notebookCommon.ts | 3 + .../notebook/common/notebookOutputRenderer.ts | 18 +- 6 files changed, 264 insertions(+), 240 deletions(-) diff --git a/extensions/notebook-markdown-extensions/package.json b/extensions/notebook-markdown-extensions/package.json index a68a8b07114..ef3911f2eb9 100644 --- a/extensions/notebook-markdown-extensions/package.json +++ b/extensions/notebook-markdown-extensions/package.json @@ -25,24 +25,18 @@ { "id": "markdownItRenderer-katex", "displayName": "Markdown it katex renderer", - "entrypoint": "./notebook-out/katex.js", - "mimeTypes": [ - "text/markdown" - ], - "dependencies": [ - "markdownItRenderer" - ] + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/katex.js" + } }, { "id": "markdownItRenderer-emoji", "displayName": "Markdown it emoji renderer", - "entrypoint": "./notebook-out/emoji.js", - "mimeTypes": [ - "text/markdown" - ], - "dependencies": [ - "markdownItRenderer" - ] + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/emoji.js" + } } ] }, diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index de640c7926e..959e70bf586 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -6,7 +6,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorPriority, NotebookRendererEntrypoint } from 'vs/workbench/contrib/notebook/common/notebookCommon'; namespace NotebookEditorContribution { export const viewType = 'viewType'; @@ -37,7 +37,7 @@ export interface INotebookRendererContribution { readonly [NotebookRendererContribution.viewType]?: string; readonly [NotebookRendererContribution.displayName]: string; readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; - readonly [NotebookRendererContribution.entrypoint]: string; + readonly [NotebookRendererContribution.entrypoint]: NotebookRendererEntrypoint; readonly [NotebookRendererContribution.hardDependencies]: readonly string[]; readonly [NotebookRendererContribution.optionalDependencies]: readonly string[]; } @@ -130,8 +130,27 @@ const notebookRendererContribution: IJSONSchema = { } }, [NotebookRendererContribution.entrypoint]: { - type: 'string', description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + oneOf: [ + { + type: 'string', + }, + // todo@connor4312 + @mjbvz: uncomment this once it's ready for external adoption + // { + // type: 'object', + // required: ['extends', 'path'], + // properties: { + // extends: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint.extends', 'Existing renderer that this one extends.'), + // }, + // path: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + // }, + // } + // } + ] }, [NotebookRendererContribution.hardDependencies]: { type: 'array', diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 8826726a081..a48496cf9e4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -203,7 +203,7 @@ export interface ICreationRequestMessage { cellTop: number; outputOffset: number; left: number; - requiredPreloads: ReadonlyArray; + requiredPreloads: ReadonlyArray; readonly initiallyHidden?: boolean; rendererId?: string | undefined; } @@ -263,17 +263,15 @@ export interface IAckOutputHeightMessage { height: number; } -export type PreloadSource = 'kernel' | { rendererId: string }; -export interface IPreloadResource { +export interface IControllerPreload { originalUri: string; uri: string; - source: PreloadSource; } -export interface IUpdatePreloadResourceMessage { +export interface IUpdateControllerPreloadsMessage { type: 'preload'; - resources: IPreloadResource[]; + resources: IControllerPreload[]; } export interface IUpdateDecorationsMessage { @@ -376,7 +374,7 @@ export type ToWebviewMessage = | IClearOutputRequestMessage | IHideOutputMessage | IShowOutputMessage - | IUpdatePreloadResourceMessage + | IUpdateControllerPreloadsMessage | IUpdateDecorationsMessage | ICustomKernelMessage | ICreateMarkdownMessage @@ -767,7 +765,7 @@ export class BackLayerWebView extends Disposable { id: renderer.id, entrypoint, mimeTypes: renderer.mimeTypes, - dependencies: Array.from(renderer.dependencies.values()) + extends: renderer.extends, }; }); } @@ -1194,7 +1192,6 @@ var requirejs = (function() { if (this._currentKernel) { this._updatePreloadsFromKernel(this._currentKernel); } - this.updateRendererPreloads(renderers); for (const [output, inset] of this.insetMapping.entries()) { this._sendMessageToWebview({ ...inset.cachedCreation, initiallyHidden: this.hiddenInsetMapping.has(output) }); @@ -1469,7 +1466,6 @@ var requirejs = (function() { ...messageBase, outputId: output.outputId, rendererId: content.renderer.id, - requiredPreloads: await this.updateRendererPreloads([content.renderer]), content: { type: RenderOutputType.Extension, outputId: output.outputId, @@ -1600,13 +1596,13 @@ var requirejs = (function() { } private _updatePreloadsFromKernel(kernel: INotebookKernel) { - const resources: IPreloadResource[] = []; + const resources: IControllerPreload[] = []; for (const preload of kernel.preloadUris) { const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https') ? preload : this.asWebviewUri(preload, undefined); if (!this._preloadsCache.has(uri.toString())) { - resources.push({ uri: uri.toString(), originalUri: preload.toString(), source: 'kernel' }); + resources.push({ uri: uri.toString(), originalUri: preload.toString() }); this._preloadsCache.add(uri.toString()); } } @@ -1618,43 +1614,7 @@ var requirejs = (function() { this._updatePreloads(resources); } - async updateRendererPreloads(renderers: Iterable) { - if (this._disposed) { - return []; - } - - const requiredPreloads: IPreloadResource[] = []; - const resources: IPreloadResource[] = []; - const extensionLocations: URI[] = []; - for (const rendererInfo of renderers) { - extensionLocations.push(rendererInfo.extensionLocation); - for (const preload of [rendererInfo.entrypoint, ...rendererInfo.preloads]) { - const uri = this.asWebviewUri(preload, rendererInfo.extensionLocation); - const resource: IPreloadResource = { - uri: uri.toString(), - originalUri: preload.toString(), - source: { rendererId: rendererInfo.id }, - }; - - requiredPreloads.push(resource); - - if (!this._preloadsCache.has(uri.toString())) { - resources.push(resource); - this._preloadsCache.add(uri.toString()); - } - } - } - - if (!resources.length) { - return requiredPreloads; - } - - this.rendererRootsCache = extensionLocations; - this._updatePreloads(resources); - return requiredPreloads; - } - - private _updatePreloads(resources: IPreloadResource[]) { + private _updatePreloads(resources: IControllerPreload[]) { if (!this.webview) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 48ce5768e1d..e1df7bf0485 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -129,10 +129,12 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } function createRendererContext(rendererId: string): RendererContext { - const api = acquireNotebookRendererApi(rendererId); return { - getState: api.getState.bind(api), - setState: api.setState.bind(api), + setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }), + getState: () => { + const state = vscode.getState(); + return typeof state === 'object' && state ? state[rendererId] as T : undefined; + }, getRenderer: (id: string) => renderers.getRenderer(id), }; } @@ -162,16 +164,39 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend if (isModule) { return __import(url); } else { + return createBackCompatModule(rendererId, text); + } + }; + + const createBackCompatModule = (rendererId: string, scriptText: string): ScriptModule => ({ + activate: (): RendererApi => { + const onDidCreateOutput = createEmitter(); + const onWillDestroyOutput = createEmitter(); + + const globals = { + acquireNotebookRendererApi: (): GlobalNotebookRendererApi => ({ + onDidCreateOutput: onDidCreateOutput.event, + onWillDestroyOutput: onWillDestroyOutput.event, + setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }), + getState: () => { + const state = vscode.getState(); + return typeof state === 'object' && state ? state[rendererId] as T : undefined; + }, + }), + }; + + invokeSourceWithGlobals(scriptText, globals); + return { - activate: () => { - const globals = { - acquireNotebookRendererApi: () => acquireNotebookRendererApi(rendererId) - }; - return invokeSourceWithGlobals(text, globals); + renderCell(id, context) { + onDidCreateOutput.fire({ ...context, outputId: id }); + }, + destroyCell(id) { + onWillDestroyOutput.fire(id ? { outputId: id } : undefined); } }; } - }; + }); const dimensionUpdater = new class { private readonly pending = new Map(); @@ -388,8 +413,6 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend focusTrackers.set(outputId, new FocusTracker(element, outputId)); } - const dontEmit = Symbol('dontEmit'); - function createEmitter(listenerChange: (listeners: Set>) => void = () => undefined): EmitterLike { const listeners = new Set>(); return { @@ -421,24 +444,16 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend }; } - // Maps the events in the given emitter, invoking mapFn on each one. mapFn can return - // the dontEmit symbol to skip emission. - function mapEmitter(emitter: EmitterLike, mapFn: (data: T) => R | typeof dontEmit) { - let listener: IDisposable; - const mapped = createEmitter(listeners => { - if (listeners.size && !listener) { - listener = emitter.event(data => { - const v = mapFn(data); - if (v !== dontEmit) { - mapped.fire(v); - } - }); - } else if (listener && !listeners.size) { - listener.dispose(); - } - }); - - return mapped.event; + function showPreloadErrors(outputNode: HTMLElement, ...errors: readonly Error[]) { + outputNode.innerText = `Error loading preloads:`; + const errList = document.createElement('ul'); + for (const result of errors) { + console.error(result); + const item = document.createElement('li'); + item.innerText = result.message; + errList.appendChild(item); + } + outputNode.appendChild(errList); } interface ICreateCellInfo { @@ -454,10 +469,9 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend outputId: string; } - const onWillDestroyOutput = createEmitter<'all' | { rendererId: string, info: IDestroyCellInfo }>(); - const onDidCreateOutput = createEmitter<{ rendererId: string, info: ICreateCellInfo }>(); const onDidReceiveKernelMessage = createEmitter(); + /** @deprecated */ interface GlobalNotebookRendererApi { setState: (newState: T) => void; getState(): T | undefined; @@ -465,65 +479,12 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend readonly onDidCreateOutput: Event; } - const acquireNotebookRendererApi = (id: string): GlobalNotebookRendererApi => ({ - setState(newState: T) { - vscode.setState({ ...vscode.getState(), [id]: newState }); - }, - getState(): T | undefined { - const state = vscode.getState(); - return typeof state === 'object' && state ? state[id] as T : undefined; - }, - onWillDestroyOutput: mapEmitter(onWillDestroyOutput, (evt) => { - if (evt === 'all') { - return undefined; - } - return evt.rendererId === id ? evt.info : dontEmit; - }), - onDidCreateOutput: mapEmitter(onDidCreateOutput, ({ rendererId, info }) => rendererId === id ? info : dontEmit), - }); - const kernelPreloadGlobals = { acquireVsCodeApi, onDidReceiveKernelMessage: onDidReceiveKernelMessage.event, postKernelMessage: (data: unknown) => postNotebookMessage('customKernelMessage', { message: data }), }; - const enum PreloadState { - Ok, - Error - } - - type PreloadResult = { state: PreloadState.Ok } | { state: PreloadState.Error, error: string }; - - /** - * Map of preload resource URIs to promises that resolve one the resource - * loads or errors. - */ - const preloadPromises = new Map>(); - const queuedOuputActions = new Map>(); - - /** - * Enqueues an action that affects a output. This blocks behind renderer load - * requests that affect the same output. This should be called whenever you - * do something that affects output to ensure it runs in - * the correct order. - */ - const enqueueOutputAction = (event: T, fn: (event: T) => Promise | void) => { - const queued = queuedOuputActions.get(event.outputId); - const maybePromise = queued ? queued.then(() => fn(event)) : fn(event); - if (typeof maybePromise === 'undefined') { - return; // a synchonrously-called function, we're done - } - - const promise = maybePromise.then(() => { - if (queuedOuputActions.get(event.outputId) === promise) { - queuedOuputActions.delete(event.outputId); - } - }); - - queuedOuputActions.set(event.outputId, promise); - }; - const ttPolicy = window.trustedTypes?.createPolicy('notebookOutputRenderer', { createHTML: value => value, createScript: value => value, @@ -605,10 +566,15 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } } break; - case 'html': - enqueueOutputAction(event.data, async data => { - const preloadResults = await Promise.all(data.requiredPreloads.map(p => preloadPromises.get(p.uri))); - if (!queuedOuputActions.has(data.outputId)) { // output was cleared while loading + case 'html': { + const data = event.data; + outputs.enqueue(event.data.outputId, async (state) => { + const preloadsAndErrors = await Promise.all([ + data.rendererId ? renderers.load(data.rendererId) : undefined, + ...data.requiredPreloads.map(p => kernelPreloads.waitFor(p.uri)), + ].map(p => p?.catch(err => err))); + + if (state.cancelled) { return; } @@ -658,38 +624,26 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend if (content.type === RenderOutputType.Html) { const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent; outputNode.innerHTML = trustedHtml as string; - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); domEval(outputNode); - } else if (preloadResults.some(e => e?.state === PreloadState.Error)) { - outputNode.innerText = `Error loading preloads:`; - const errList = document.createElement('ul'); - for (const result of preloadResults) { - if (result?.state === PreloadState.Error) { - const item = document.createElement('li'); - item.innerText = result.error; - errList.appendChild(item); - } - } - outputNode.appendChild(errList); - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); + } else if (preloadsAndErrors.some(e => e instanceof Error)) { + const errors = preloadsAndErrors.filter((e): e is Error => e instanceof Error); + showPreloadErrors(outputNode, ...errors); } else { - // TODO: this should go through renderers instead - onDidCreateOutput.fire({ - rendererId: data.rendererId!, - info: { + const rendererApi = preloadsAndErrors[0] as RendererApi; + try { + rendererApi.renderCell(outputId, { element: outputNode, - outputId, mime: content.mimeType, value: content.value, metadata: content.metadata, - } - }); - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); + }); + } catch (e) { + showPreloadErrors(outputNode, e); + } } + cellOutputContainer.appendChild(outputContainer); + outputContainer.appendChild(outputNode); resizeObserver.observe(outputNode, outputId, true); const clientHeight = outputNode.clientHeight; @@ -714,6 +668,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend cellOutputContainer.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; }); break; + } case 'view-scroll': { // const date = new Date(); @@ -740,8 +695,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend break; } case 'clear': - queuedOuputActions.clear(); // stop all loading outputs - onWillDestroyOutput.fire('all'); + renderers.clearAll(); document.getElementById('container')!.innerText = ''; focusTrackers.forEach(ft => { @@ -753,26 +707,29 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend const output = document.getElementById(event.data.outputId); const { rendererId, outputId } = event.data; - queuedOuputActions.delete(outputId); // stop any in-progress rendering + outputs.cancelOutput(outputId); if (output && output.parentNode) { if (rendererId) { - onWillDestroyOutput.fire({ rendererId, info: { outputId } }); + renderers.clearOutput(rendererId, outputId); } output.parentNode.removeChild(output); } break; } - case 'hideOutput': - enqueueOutputAction(event.data, ({ outputId }) => { + case 'hideOutput': { + const { outputId } = event.data; + outputs.enqueue(event.data.outputId, () => { const container = document.getElementById(outputId)?.parentElement?.parentElement; if (container) { container.style.visibility = 'hidden'; } }); break; - case 'showOutput': - enqueueOutputAction(event.data, ({ outputId, cellTop: top, }) => { + } + case 'showOutput': { + const { outputId, cellTop: top } = event.data; + outputs.enqueue(event.data.outputId, () => { const output = document.getElementById(outputId); if (output) { output.parentElement!.parentElement!.style.visibility = 'visible'; @@ -784,6 +741,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } }); break; + } case 'ack-dimension': { const { outputId, height } = event.data; @@ -796,35 +754,8 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } case 'preload': const resources = event.data.resources; - let queue: Promise = Promise.resolve({ state: PreloadState.Ok }); - for (const { uri, originalUri, source } of resources) { - // create the promise so that the scripts download in parallel, but - // only invoke them in series within the queue - - if (source === 'kernel') { - const promise = runPreload(uri, originalUri); - queue = queue.then(() => promise.then(async module => { - try { - await module.activate(); - return { state: PreloadState.Ok }; - } catch (error) { - console.error(error); - return { state: PreloadState.Error, error: error.toString() }; - } - })); - } else { - queue = queue.then(async () => { - try { - await renderers.load(source.rendererId); - return { state: PreloadState.Ok }; - } catch (error) { - console.error(error); - return { state: PreloadState.Error, error: error.toString() }; - } - }); - } - - preloadPromises.set(uri, queue); + for (const { uri, originalUri } of resources) { + kernelPreloads.load(uri, originalUri); } break; case 'focus-output': @@ -863,12 +794,13 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend interface RendererApi { renderCell: (id: string, context: ICreateCellInfo) => void; + destroyCell?: (id?: string) => void; } class Renderer { constructor( public readonly data: RendererMetadata, - private readonly loadDependency: (id: string) => Promise, + private readonly loadExtension: (id: string) => Promise, ) { } private _loadPromise: Promise | undefined; @@ -878,33 +810,116 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend public load(): Promise { if (!this._loadPromise) { - this._loadPromise = Promise.all(this.data.dependencies.map(dependencyId => this.loadDependency(dependencyId))) - .then(() => runRenderScript(this.data.entrypoint, this.data.id)) - .then(module => { - if (module) { - const api = module.activate(createRendererContext(this.data.id)); - this._api = api; - return api; - } - return undefined; - }); + this._loadPromise = this._load(); } + return this._loadPromise; } + + /** Inner function cached in the _loadPromise(). */ + private async _load() { + const module = await runRenderScript(this.data.entrypoint, this.data.id); + if (!module) { + return; + } + + const api = module.activate(createRendererContext(this.data.id)); + this._api = api; + + // Squash any errors extends errors. They won't prevent the renderer + // itself from working, so just log them. + await Promise.all(rendererData + .filter(d => d.extends === this.data.id) + .map(d => this.loadExtension(d.id).catch(console.error)), + ); + + return api; + } } - const renderers = new class { + const kernelPreloads = new class { + private readonly preloads = new Map>(); + /** + * Returns a promise that resolves when the given preload is activated. + */ + public waitFor(uri: string) { + return this.preloads.get(uri) || Promise.resolve(new Error(`Preload not ready: ${uri}`)); + } + + /** + * Loads a preload. + * @param uri URI to load from + * @param originalUri URI to show in an error message if the preload is invalid. + */ + public load(uri: string, originalUri: string) { + const promise = Promise.all([ + runPreload(uri, originalUri), + this.waitForAllCurrent(), + ]).then(([module]) => module.activate()); + + this.preloads.set(uri, promise); + return promise; + } + + /** + * Returns a promise that waits for all currently-registered preloads to + * activate before resolving. + */ + private waitForAllCurrent() { + return Promise.all([...this.preloads.values()].map(p => p.catch(err => err))); + } + }; + + const outputs = new class { + private outputs = new Map }>(); + /** + * Pushes the action onto the list of actions for the given output ID, + * ensuring that it's run in-order. + */ + public enqueue(outputId: string, action: (record: { cancelled: boolean }) => unknown) { + const record = this.outputs.get(outputId); + if (!record) { + this.outputs.set(outputId, { cancelled: false, queue: new Promise(r => r(action({ cancelled: false }))) }); + } else { + record.queue = record.queue.then(r => !record.cancelled && action(record)); + } + } + + /** + * Cancells the rendering of all outputs. + */ + public cancelAll() { + for (const record of this.outputs.values()) { + record.cancelled = true; + } + this.outputs.clear(); + } + + /** + * Cancels any ongoing rendering out an output. + */ + public cancelOutput(outputId: string) { + const output = this.outputs.get(outputId); + if (output) { + output.cancelled = true; + this.outputs.delete(outputId); + } + } + }; + + const renderers = new class { private readonly _renderers = new Map(); constructor() { for (const renderer of rendererData) { - this._renderers.set(renderer.id, new Renderer(renderer, async (dependencyId) => { - const parent = this._renderers.get(dependencyId); - if (!parent) { - throw new Error(`Could not find renderer dependency: ${dependencyId}`); + this._renderers.set(renderer.id, new Renderer(renderer, async (extensionId) => { + const ext = this._renderers.get(extensionId); + if (!ext) { + throw new Error(`Could not find extending renderer: ${extensionId}`); } - await parent.load(); + + await ext.load(); })); } } @@ -913,7 +928,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend return this._renderers.get(id)?.api; } - public load(id: string) { + public async load(id: string) { const renderer = this._renderers.get(id); if (!renderer) { throw new Error('Could not find renderer'); @@ -922,9 +937,31 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend return renderer.load(); } + + public clearAll() { + outputs.cancelAll(); + for (const renderer of this._renderers.values()) { + renderer.api?.destroyCell?.(); + } + } + + public clearOutput(rendererId: string, outputId: string) { + outputs.cancelOutput(outputId); + this._renderers.get(rendererId)?.api?.destroyCell?.(outputId); + } + + public async renderCustom(rendererId: string, outputId: string, info: ICreateCellInfo) { + const api = await this.load(rendererId); + if (!api) { + throw new Error(`renderer ${rendererId} did not return an API`); + } + + api.renderCell(outputId, info); + } + public async renderMarkdown(id: string, element: HTMLElement, content: string): Promise { const markdownRenderers = Array.from(this._renderers.values()) - .filter(renderer => renderer.data.mimeTypes.includes('text/markdown')); + .filter(renderer => renderer.data.mimeTypes.includes('text/markdown') && !renderer.data.extends); if (!markdownRenderers.length) { throw new Error('Could not find renderer'); @@ -932,8 +969,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend await Promise.all(markdownRenderers.map(x => x.load())); - const renderer = Array.from(this._renderers.values()).find(x => x.data.mimeTypes.includes('text/markdown')); - renderer?.api?.renderCell(id, { + markdownRenderers[0].api?.renderCell(id, { element, value: content, mime: 'text/markdown', @@ -1172,8 +1208,8 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend export interface RendererMetadata { readonly id: string; readonly entrypoint: string; - readonly dependencies: readonly string[] readonly mimeTypes: readonly string[]; + readonly extends: string | undefined; } export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly RendererMetadata[]) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 816f3dbaf20..354e926d95b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -53,6 +53,8 @@ export const ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER = [ export const BUILTIN_RENDERER_ID = '_builtin'; export const RENDERER_NOT_AVAILABLE = '_notAvailable'; +export type NotebookRendererEntrypoint = string | { extends: string; path: string }; + export enum NotebookRunState { Running = 1, Idle = 2 @@ -132,6 +134,7 @@ export const enum NotebookRendererMatch { export interface INotebookRendererInfo { id: string; displayName: string; + extends?: string; entrypoint: URI; preloads: ReadonlyArray; extensionLocation: URI; diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index b698cd5b3ee..5025db38aeb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -8,7 +8,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { INotebookRendererInfo, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookRendererInfo, NotebookRendererEntrypoint, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; class DependencyList { private readonly value: ReadonlySet; @@ -34,6 +34,7 @@ class DependencyList { export class NotebookOutputRendererInfo implements INotebookRendererInfo { readonly id: string; + readonly extends?: string; readonly entrypoint: URI; readonly displayName: string; readonly extensionLocation: URI; @@ -49,7 +50,7 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { constructor(descriptor: { readonly id: string; readonly displayName: string; - readonly entrypoint: string; + readonly entrypoint: NotebookRendererEntrypoint; readonly mimeTypes: readonly string[]; readonly extension: IExtensionDescription; readonly dependencies: readonly string[] | undefined; @@ -58,7 +59,14 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { this.id = descriptor.id; this.extensionId = descriptor.extension.identifier; this.extensionLocation = descriptor.extension.extensionLocation; - this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); + + if (typeof descriptor.entrypoint === 'string') { + this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); + } else { + this.extends = descriptor.entrypoint.extends; + this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint.path); + } + this.displayName = descriptor.displayName; this.mimeTypes = descriptor.mimeTypes; this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern)); @@ -103,6 +111,10 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { } private matchesMimeTypeOnly(mimeType: string) { + if (this.extends !== undefined) { + return false; + } + return this.mimeTypeGlobs.some(pattern => pattern(mimeType)) || this.mimeTypes.some(pattern => pattern === mimeType); } } From c59f782cc7db2a43d4f3393d7dc3304cccccfc79 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Tue, 18 May 2021 17:07:17 -0700 Subject: [PATCH 066/270] adopt table widget for workspace trust editor --- .../browser/workspaceTrustEditor.css | 88 ++- .../workspace/browser/workspaceTrustEditor.ts | 535 +++++++++++++-- .../workspace/browser/workspaceTrustTree.ts | 619 ------------------ 3 files changed, 535 insertions(+), 707 deletions(-) delete mode 100644 src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css index ce18a414f9d..63f05bb1fd4 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css @@ -196,41 +196,6 @@ flex: 1; } -/** Settings */ -.workspace-trust-editor .workspace-trust-settings .workspace-trust-section-title { - padding: 14px; -} - -.workspace-trust-editor .settings-editor .settings-body { - margin-top: 0; -} - -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .shadow.top { - left: initial; - margin-left: initial; - max-width: initial; -} - -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-rows { - background: unset; -} - -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents { - padding-left: 0; - padding-right: 0; -} - -.workspace-trust-editor .settings-editor .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents .setting-list-edit-row > .setting-list-valueInput { - width: 100%; - max-width: 100%; -} - -.workspace-trust-editor .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-key, -.workspace-trust-editor .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-input-key { - margin-left: 4px; - min-width: 20%; -} - .workspace-trust-intro-dialog { min-width: min(50vw, 500px); padding-right: 24px; @@ -250,3 +215,56 @@ max-height: 32px; padding-right: 10px; } + +.workspace-trust-editor .workspace-trust-settings { + padding-top: 20px; + padding-bottom: 20px; +} + +.workspace-trust-editor .workspace-trust-settings .workspace-trusted-folders-title { + font-weight: 600; +} + +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path:not(.input-mode) .monaco-inputbox, +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path.input-mode .path-label { + display: none; +} + +.workspace-trust-editor .monaco-table-tr .monaco-table-td .current-workspace-parent .path-label, +.workspace-trust-editor .monaco-table-tr .monaco-table-td .current-workspace-parent .host-label { + font-weight: bold; + font-style: italic; +} + + +.workspace-trust-editor .monaco-table-tr .monaco-table-td .path .monaco-inputbox input { + padding-left: 5px; +} + +.workspace-trust-editor .monaco-table-th, +.workspace-trust-editor .monaco-table-td { + padding-left: 5px; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-action-bar .action-item > .codicon { + display: flex; + align-items: center; + justify-content: center; + color: inherit; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td { + align-items: center; + display: flex; + overflow: hidden; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .path { + width: 100%; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .monaco-button { + height: 18px; + padding-left: 8px; + padding-right: 8px; +} diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index eab4501118c..08da9da034b 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -3,35 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, append, clearNode, Dimension, EventHelper } from 'vs/base/browser/dom'; +import { $, addDisposableListener, addStandardDisposableListener, append, clearNode, Dimension, EventHelper, EventType } from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ButtonBar } from 'vs/base/browser/ui/button/button'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { Action } from 'vs/base/common/actions'; +import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { Action, IAction } from 'vs/base/common/actions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon, registerCodicon } from 'vs/base/common/codicons'; import { debounce } from 'vs/base/common/decorators'; -import { Iterable } from 'vs/base/common/iterator'; +import { Emitter, Event } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { splitName } from 'vs/base/common/labels'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { Schemas } from 'vs/base/common/network'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { isArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionUntrustedWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { WorkbenchTable } from 'vs/platform/list/browser/listService'; +import { IPromptChoiceWithMenu } from 'vs/platform/notification/common/notification'; import { Link } from 'vs/platform/opener/browser/link'; import product from 'vs/platform/product/common/product'; import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { buttonBackground, buttonSecondaryBackground, editorErrorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { attachButtonStyler, attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; @@ -41,9 +46,10 @@ import { ChoiceAction } from 'vs/workbench/common/notifications'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { getInstalledExtensions, IExtensionStatus } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; -import { IWorkspaceTrustSettingChangeEvent, WorkspaceTrustSettingArrayRenderer, WorkspaceTrustTree, WorkspaceTrustTreeModel } from 'vs/workbench/contrib/workspace/browser/workspaceTrustTree'; +import { settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { filterSettingsRequireWorkspaceTrust, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; export const shieldIcon = registerCodicon('workspace-trust-icon', Codicon.shield); @@ -51,6 +57,461 @@ export const shieldIcon = registerCodicon('workspace-trust-icon', Codicon.shield const checkListIcon = registerCodicon('workspace-trusted-check-icon', Codicon.check); const xListIcon = registerCodicon('workspace-trusted-x-icon', Codicon.x); +const enum TrustedUriItemType { + Existing = 1, + Add = 2 +} + +interface ITrustedUriItem { + entryType: TrustedUriItemType; + parentOfWorkspaceItem: boolean; + uri: URI; +} + +class WorkspaceTrustedUrisTable extends Disposable { + private readonly _onDidAcceptEdit: Emitter = this._register(new Emitter()); + readonly onDidAcceptEdit: Event = this._onDidAcceptEdit.event; + + private readonly _onDidRejectEdit: Emitter = this._register(new Emitter()); + readonly onDidRejectEdit: Event = this._onDidRejectEdit.event; + + private _onEdit: Emitter = this._register(new Emitter()); + readonly onEdit: Event = this._onEdit.event; + + private _onDelete: Emitter = this._register(new Emitter()); + readonly onDelete: Event = this._onDelete.event; + + private readonly table: WorkbenchTable; + + constructor( + private readonly container: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IUriIdentityService private readonly uriService: IUriIdentityService, + @IFileDialogService private readonly fileDialogService: IFileDialogService + ) { + super(); + + this.table = this.instantiationService.createInstance( + WorkbenchTable, + 'WorkspaceTrust', + this.container, + new TrustedUriTableVirtualDelegate(), + [ + { + label: localize('hostColumnLabel', "Host"), + tooltip: '', + weight: 1, + templateId: TrustedUriHostColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + { + label: localize('pathColumnLabel', "Path"), + tooltip: '', + weight: 9, + templateId: TrustedUriPathColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + { + label: '', + tooltip: '', + weight: 0, + minimumWidth: 80, + maximumWidth: 80, + templateId: TrustedUriActionsColumnRenderer.TEMPLATE_ID, + project(row: ITrustedUriItem): ITrustedUriItem { return row; } + }, + ], + [ + this.instantiationService.createInstance(TrustedUriHostColumnRenderer, this), + this.instantiationService.createInstance(TrustedUriPathColumnRenderer, this), + this.instantiationService.createInstance(TrustedUriActionsColumnRenderer, this), + ], + { + horizontalScrolling: false, + openOnSingleClick: false, + } + ) as WorkbenchTable; + + this._register(this.table.onDidOpen(item => { + if (item && item.element) { + this.edit(item.element); + } + })); + + this._register(this.workspaceTrustManagementService.onDidChangeTrustedFolders(() => { + this.updateTable(); + })); + } + + private getIndexOfTrustedUriEntry(item: ITrustedUriItem): number { + const index = this.trustedUriEntries.indexOf(item); + if (index === -1) { + for (let i = 0; i < this.trustedUriEntries.length; i++) { + if (this.trustedUriEntries[i].entryType !== item.entryType) { + continue; + } + + if (item.entryType === TrustedUriItemType.Add || this.trustedUriEntries[i].uri === item.uri) { + return i; + } + } + } + + return index; + } + + private selectTrustedUriEntry(item: ITrustedUriItem, focus: boolean = true): void { + const index = this.getIndexOfTrustedUriEntry(item); + if (index !== -1) { + if (focus) { + this.table.domFocus(); + this.table.setFocus([index]); + } + this.table.setSelection([index]); + } + } + + private get trustedUriEntries(): ITrustedUriItem[] { + const currentWorkspace = this.workspaceService.getWorkspace(); + const currentWorkspaceUri = currentWorkspace.folders[0]?.uri || URI.file('/'); + const currentWorkspaceUris = currentWorkspace.folders.map(folder => folder.uri); + if (currentWorkspace.configuration) { + currentWorkspaceUris.push(currentWorkspace.configuration); + } + + const entries = this.workspaceTrustManagementService.getTrustedFolders().map(uri => { + + let relatedToCurrentWorkspace = false; + for (const workspaceUri of currentWorkspaceUris) { + relatedToCurrentWorkspace = relatedToCurrentWorkspace || this.uriService.extUri.isEqualOrParent(workspaceUri, uri); + } + + return { + uri, + entryType: TrustedUriItemType.Existing, + parentOfWorkspaceItem: relatedToCurrentWorkspace + }; + }); + entries.push({ uri: currentWorkspaceUri, entryType: TrustedUriItemType.Add, parentOfWorkspaceItem: false }); + return entries; + } + + layout(): void { + this.table.layout((this.trustedUriEntries.length * TrustedUriTableVirtualDelegate.ROW_HEIGHT) + TrustedUriTableVirtualDelegate.HEADER_ROW_HEIGHT, undefined); + } + + updateTable(): void { + this.table.splice(0, Number.POSITIVE_INFINITY, this.trustedUriEntries); + this.layout(); + } + + acceptEdit(item: ITrustedUriItem, uri: URI) { + const trustedFolders = this.workspaceTrustManagementService.getTrustedFolders(); + const index = this.getIndexOfTrustedUriEntry(item); + + if (index >= trustedFolders.length) { + trustedFolders.push(uri); + } else { + trustedFolders[index] = uri; + } + + this.workspaceTrustManagementService.setTrustedFolders(trustedFolders); + this._onDidAcceptEdit.fire(item); + } + + rejectEdit(item: ITrustedUriItem) { + this._onDidRejectEdit.fire(item); + } + + delete(item: ITrustedUriItem) { + this.workspaceTrustManagementService.setUrisTrust([item.uri], false); + this._onDelete.fire(item); + } + + async edit(item: ITrustedUriItem) { + if (item.uri.scheme === Schemas.file || item.uri.scheme === Schemas.vscodeRemote) { + const uri = await this.fileDialogService.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: true, + canSelectMany: false, + defaultUri: item.uri, + openLabel: localize('trustUri', "Trust Folder"), + title: localize('selectTrustedUri', "Select Folder To Trust") + }); + + if (uri) { + this.acceptEdit(item, uri[0]); + } else { + this.rejectEdit(item); + } + } else { + this.selectTrustedUriEntry(item); + this._onEdit.fire(item); + } + } +} + +class TrustedUriTableVirtualDelegate implements ITableVirtualDelegate { + static readonly HEADER_ROW_HEIGHT = 30; + static readonly ROW_HEIGHT = 24; + readonly headerRowHeight = TrustedUriTableVirtualDelegate.HEADER_ROW_HEIGHT; + getHeight(item: ITrustedUriItem) { + return TrustedUriTableVirtualDelegate.ROW_HEIGHT; + } +} + +interface IActionsColumnTemplateData { + readonly actionBar: ActionBar; +} + +class TrustedUriActionsColumnRenderer implements ITableRenderer { + + static readonly TEMPLATE_ID = 'actions'; + + readonly templateId: string = TrustedUriActionsColumnRenderer.TEMPLATE_ID; + + constructor(private readonly table: WorkspaceTrustedUrisTable) { } + + renderTemplate(container: HTMLElement): IActionsColumnTemplateData { + const element = container.appendChild($('.actions')); + const actionBar = new ActionBar(element, { animated: false }); + return { actionBar }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: IActionsColumnTemplateData, height: number | undefined): void { + templateData.actionBar.clear(); + + if (item.entryType !== TrustedUriItemType.Add) { + const actions: IAction[] = []; + actions.push(this.createEditAction(item)); + actions.push(this.createDeleteAction(item)); + templateData.actionBar.push(actions, { icon: true }); + } + } + + private createEditAction(item: ITrustedUriItem): IAction { + return { + class: ThemeIcon.asClassName(settingsEditIcon), + enabled: true, + id: 'editTrustedUri', + tooltip: localize('editTrustedUri', "Change Path"), + run: () => { + this.table.edit(item); + } + }; + } + + private createDeleteAction(item: ITrustedUriItem): IAction { + return { + class: ThemeIcon.asClassName(settingsRemoveIcon), + enabled: true, + id: 'deleteTrustedUri', + tooltip: localize('deleteTrustedUri', "Delete Path"), + run: () => { + this.table.delete(item); + } + }; + } + + disposeTemplate(templateData: IActionsColumnTemplateData): void { + templateData.actionBar.dispose(); + } + +} + +interface ITrustedUriPathColumnTemplateData { + element: HTMLElement; + pathLabel: HTMLElement; + pathInput: InputBox; + renderDisposables: DisposableStore; + disposables: DisposableStore; +} + +class TrustedUriPathColumnRenderer implements ITableRenderer { + static readonly TEMPLATE_ID = 'path'; + + readonly templateId: string = TrustedUriPathColumnRenderer.TEMPLATE_ID; + + constructor( + private readonly table: WorkspaceTrustedUrisTable, + @IContextViewService private readonly contextViewService: IContextViewService, + @IThemeService private readonly themeService: IThemeService, + ) { + } + + renderTemplate(container: HTMLElement): ITrustedUriPathColumnTemplateData { + const element = container.appendChild($('.path')); + const pathLabel = element.appendChild($('div.path-label')); + + const pathInput = new InputBox(element, this.contextViewService); + + const disposables = new DisposableStore(); + disposables.add(attachInputBoxStyler(pathInput, this.themeService)); + + const renderDisposables = disposables.add(new DisposableStore()); + + return { + element, + pathLabel, + pathInput, + disposables, + renderDisposables + }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: ITrustedUriPathColumnTemplateData, height: number | undefined): void { + templateData.renderDisposables.clear(); + + templateData.renderDisposables.add(this.table.onEdit(async (e) => { + if (item === e) { + templateData.element.classList.add('input-mode'); + templateData.pathInput.focus(); + templateData.pathInput.select(); + templateData.element.parentElement!.style.paddingLeft = '0px'; + } + })); + + + const hideInputBox = () => { + templateData.element.classList.remove('input-mode'); + templateData.element.parentElement!.style.paddingLeft = '5px'; + }; + + const accept = () => { + hideInputBox(); + const uri = item.uri.with({ path: templateData.pathInput.value }); + templateData.pathLabel.innerText = templateData.pathInput.value; + + if (uri) { + this.table.acceptEdit(item, uri); + } + }; + + const reject = () => { + hideInputBox(); + templateData.pathInput.value = stringValue; + this.table.rejectEdit(item); + }; + + templateData.renderDisposables.add(addStandardDisposableListener(templateData.pathInput.inputElement, EventType.KEY_DOWN, e => { + let handled = false; + if (e.equals(KeyCode.Enter)) { + accept(); + handled = true; + } else if (e.equals(KeyCode.Escape)) { + reject(); + handled = true; + } + + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + })); + templateData.renderDisposables.add((addDisposableListener(templateData.pathInput.inputElement, EventType.BLUR, () => { + reject(); + }))); + + const stringValue = item.uri.scheme === Schemas.file ? URI.revive(item.uri).fsPath : item.uri.path; + templateData.pathInput.value = stringValue; + templateData.pathLabel.innerText = stringValue; + templateData.element.classList.toggle('current-workspace-parent', item.parentOfWorkspaceItem); + + templateData.pathLabel.style.display = item.entryType === TrustedUriItemType.Add ? 'none' : ''; + } + + disposeTemplate(templateData: ITrustedUriPathColumnTemplateData): void { + templateData.disposables.dispose(); + templateData.renderDisposables.dispose(); + } + +} + + +interface ITrustedUriHostColumnTemplateData { + element: HTMLElement; + hostContainer: HTMLElement; + buttonBarContainer: HTMLElement; + disposables: DisposableStore; + renderDisposables: DisposableStore; +} + +class TrustedUriHostColumnRenderer implements ITableRenderer { + static readonly TEMPLATE_ID = 'host'; + + readonly templateId: string = TrustedUriHostColumnRenderer.TEMPLATE_ID; + + constructor( + private readonly table: WorkspaceTrustedUrisTable, + @ILabelService private readonly labelService: ILabelService, + @IThemeService private readonly themeService: IThemeService, + ) { } + + renderTemplate(container: HTMLElement): ITrustedUriHostColumnTemplateData { + const disposables = new DisposableStore(); + const renderDisposables = disposables.add(new DisposableStore()); + + const element = container.appendChild($('.host')); + const hostContainer = element.appendChild($('div.host-label')); + const buttonBarContainer = element.appendChild($('div.button-bar')); + + return { + element, + hostContainer, + buttonBarContainer, + disposables, + renderDisposables + }; + } + + renderElement(item: ITrustedUriItem, index: number, templateData: ITrustedUriHostColumnTemplateData, height: number | undefined): void { + templateData.renderDisposables.clear(); + templateData.renderDisposables.add({ dispose: () => { clearNode(templateData.buttonBarContainer); } }); + + templateData.hostContainer.innerText = item.uri.authority ? this.labelService.getHostLabel(item.uri.scheme, item.uri.authority) : localize('localAuthority', "Local"); + templateData.element.classList.toggle('current-workspace-parent', item.parentOfWorkspaceItem); + + if (item.entryType === TrustedUriItemType.Add) { + templateData.hostContainer.style.display = 'none'; + templateData.buttonBarContainer.style.display = ''; + + const buttonBar = templateData.renderDisposables.add(new ButtonBar(templateData.buttonBarContainer)); + const addButton = templateData.renderDisposables.add(buttonBar.addButton({ title: localize('addButton', "Add Path") })); + addButton.label = localize('addButton', "Add Path"); + + templateData.renderDisposables.add(attachButtonStyler(addButton, this.themeService)); + + templateData.renderDisposables.add(addButton.onDidClick(() => { + this.table.edit(item); + })); + + templateData.renderDisposables.add(this.table.onEdit(e => { + if (item === e) { + templateData.hostContainer.style.display = ''; + templateData.buttonBarContainer.style.display = 'none'; + } + })); + + templateData.renderDisposables.add(this.table.onDidRejectEdit(e => { + if (item === e) { + templateData.hostContainer.style.display = 'none'; + templateData.buttonBarContainer.style.display = ''; + } + })); + } else { + templateData.hostContainer.style.display = ''; + templateData.buttonBarContainer.style.display = 'none'; + } + } + + disposeTemplate(templateData: ITrustedUriHostColumnTemplateData): void { + templateData.disposables.dispose(); + } + +} + export class WorkspaceTrustEditor extends EditorPane { static readonly ID: string = 'workbench.editor.workspaceTrust'; private rootElement!: HTMLElement; @@ -69,9 +530,7 @@ export class WorkspaceTrustEditor extends EditorPane { // Settings Section private configurationContainer!: HTMLElement; - private trustSettingsTree!: WorkspaceTrustTree; - private workspaceTrustSettingsTreeModel!: WorkspaceTrustTreeModel; - + private workpaceTrustedUrisTable!: WorkspaceTrustedUrisTable; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -82,7 +541,6 @@ export class WorkspaceTrustEditor extends EditorPane { @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IDialogService private readonly dialogService: IDialogService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, ) { super(WorkspaceTrustEditor.ID, telemetryService, themeService, storageService); } @@ -211,8 +669,7 @@ export class WorkspaceTrustEditor extends EditorPane { this.renderAffectedFeatures(settingsRequiringTrustedWorkspaceCount, onDemandExtensionCount + onStartExtensionCount); // Configuration Tree - this.workspaceTrustSettingsTreeModel.update(this.workspaceTrustManagementService.getTrustedFolders()); - this.trustSettingsTree.setChildren(null, Iterable.map(this.workspaceTrustSettingsTreeModel.settings, s => { return { element: s }; })); + this.workpaceTrustedUrisTable.updateTable(); this.bodyScrollBar.getDomNode().style.height = `calc(100% - ${this.headerContainer.clientHeight}px)`; this.bodyScrollBar.scanDomNode(); @@ -241,20 +698,16 @@ export class WorkspaceTrustEditor extends EditorPane { } private createConfigurationElement(parent: HTMLElement): void { - this.configurationContainer = append(parent, $('.workspace-trust-settings.settings-editor')); + this.configurationContainer = append(parent, $('.workspace-trust-settings')); + const configurationTitle = append(this.configurationContainer, $('.workspace-trusted-folders-title')); + configurationTitle.innerText = localize('trustedFolders', "Trusted Folders"); - const settingsBody = append(this.configurationContainer, $('.workspace-trust-settings-body.settings-body')); + const configurationDescription = append(this.configurationContainer, $('.workspace-trusted-folders-description')); + configurationDescription.innerText = localize('trustedFoldersDescription', "You trust the following folders and their children."); - const workspaceTrustTreeContainer = append(settingsBody, $('.workspace-trust-settings-tree-container.settings-tree-container')); - const renderer = this.instantiationService.createInstance(WorkspaceTrustSettingArrayRenderer,); + this.workpaceTrustedUrisTable = this._register(this.instantiationService.createInstance(WorkspaceTrustedUrisTable, this.configurationContainer)); - this.trustSettingsTree = this._register(this.instantiationService.createInstance(WorkspaceTrustTree, - workspaceTrustTreeContainer, - [renderer])); - this.workspaceTrustSettingsTreeModel = this.instantiationService.createInstance(WorkspaceTrustTreeModel); - - this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e))); } private createAffectedFeaturesElement(parent: HTMLElement): void { @@ -370,7 +823,7 @@ export class WorkspaceTrustEditor extends EditorPane { private addTrustedTextToElement(parent: HTMLElement): void { const textElement = append(parent, $('.workspace-trust-untrusted-description')); - textElement.innerText = this.useWorkspaceLanguage() ? localize('untrustedWorkspaceReason', "This workspace is trusted via one or more of the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via one or more of the trusted folders below."); + textElement.innerText = this.useWorkspaceLanguage() ? localize('untrustedWorkspaceReason', "This workspace is trusted via the bolded entries in the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via the bolded entries in the the trusted folders below."); } private renderLimitationsHeaderElement(parent: HTMLElement, headerText: string, subtitleText: string): void { @@ -406,37 +859,13 @@ export class WorkspaceTrustEditor extends EditorPane { } } - private onDidChangeSetting(change: IWorkspaceTrustSettingChangeEvent) { - const applyChangesWithPrompt = async (showPrompt: boolean, applyChanges: () => void) => { - if (showPrompt) { - const message = localize('workspaceTrustSettingModificationMessage', "Update Workspace Trust Settings"); - const detail = localize('workspaceTrustTransitionDetail', "In order to safely complete this action, all affected windows will have to be reloaded. Are you sure you want to proceed with this action?"); - const primaryButton = localize('workspaceTrustTransitionPrimaryButton', "Yes"); - const secondaryButton = localize('workspaceTrustTransitionSecondaryButton', "No"); - - const result = await this.dialogService.show(Severity.Info, message, [primaryButton, secondaryButton], { cancelId: 1, detail, custom: { icon: Codicon.shield } }); - if (result.choice !== 0) { - return; - } - } - - applyChanges(); - }; - - if (isArray(change.value)) { - if (change.key === 'trustedFolders') { - applyChangesWithPrompt(false, () => this.workspaceTrustManagementService.setTrustedFolders(change.value!)); - } - } - } - private layoutParticipants: { layout: () => void; }[] = []; layout(dimension: Dimension): void { if (!this.isVisible()) { return; } - this.trustSettingsTree.layout(dimension.height, dimension.width); + this.workpaceTrustedUrisTable.layout(); this.layoutParticipants.forEach(participant => { participant.layout(); diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts deleted file mode 100644 index 2c3e7d6eff5..00000000000 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts +++ /dev/null @@ -1,619 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { addDisposableListener, append, EventType, $, createStyleSheet, trackFocus, addStandardDisposableListener } from 'vs/base/browser/dom'; -import { DefaultStyleController, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; -import { IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; -import { ITreeModel, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; -import { Color, RGBA } from 'vs/base/common/color'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { isArray } from 'vs/base/common/types'; -import { localize } from 'vs/nls'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IListService, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorBackground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { NonCollapsibleObjectTreeModel } from 'vs/workbench/contrib/preferences/browser/settingsTree'; -import { AbstractListSettingWidget, focusedRowBackground, focusedRowBorder, ISettingListChangeEvent, settingsHeaderForeground, settingsSelectBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; -import { attachButtonStyler, attachInputBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; -import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IAction } from 'vs/base/common/actions'; -import { settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { Button } from 'vs/base/browser/ui/button/button'; -import { disposableTimeout } from 'vs/base/common/async'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { ILabelService } from 'vs/platform/label/common/label'; - - -export class WorkspaceTrustSettingsTreeEntry { - id: string; - displayLabel: string; - setting: { - key: string; - description: string; - }; - value: URI[]; - - constructor(key: string, displayLabel: string, description: string, value: URI[]) { - this.setting = { key, description }; - this.displayLabel = displayLabel; - this.value = value; - this.id = key; - } -} - -export interface IWorkspaceTrustSettingItemTemplate { - onChange?: (value: T, type: WorkspaceTrustSettingListItemChangeType) => void; - - toDispose: DisposableStore; - context?: WorkspaceTrustSettingsTreeEntry; - containerElement: HTMLElement; - labelElement: HTMLElement; - descriptionElement: HTMLElement; - controlElement: HTMLElement; - elementDisposables: DisposableStore; -} - -export interface IWorkspaceTrustUriDataItem extends UriComponents { } - -class WorkspaceTrustFolderSettingWidget extends AbstractListSettingWidget { - constructor( - container: HTMLElement, - @ILabelService protected readonly labelService: ILabelService, - @IThemeService themeService: IThemeService, - @IContextViewService contextViewService: IContextViewService - ) { - super(container, themeService, contextViewService); - } - - protected getEmptyItem(): IWorkspaceTrustUriDataItem { - return URI.file(''); - } - - protected getContainerClasses() { - return ['workspace-trust-uri-setting-widget', 'setting-list-object-widget']; - } - - protected getActionsForItem(item: IWorkspaceTrustUriDataItem, idx: number): IAction[] { - return [ - { - class: ThemeIcon.asClassName(settingsEditIcon), - enabled: true, - id: 'workbench.action.editListItem', - tooltip: this.getLocalizedStrings().editActionTooltip, - run: () => this.editSetting(idx) - }, - { - class: ThemeIcon.asClassName(settingsRemoveIcon), - enabled: true, - id: 'workbench.action.removeListItem', - tooltip: this.getLocalizedStrings().deleteActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) - } - ] as IAction[]; - } - - protected override renderHeader() { - const header = $('.setting-list-row-header'); - const hostHeader = append(header, $('.setting-list-object-key')); - const pathHeader = append(header, $('.setting-list-object-value')); - const { hostHeaderText, pathHeaderText } = this.getLocalizedStrings(); - - hostHeader.textContent = hostHeaderText; - pathHeader.textContent = pathHeaderText; - - return header; - } - - protected renderItem(item: IWorkspaceTrustUriDataItem): HTMLElement { - const rowElement = $('.setting-list-row'); - rowElement.classList.add('setting-list-object-row'); - - const hostElement = append(rowElement, $('.setting-list-object-key')); - const pathElement = append(rowElement, $('.setting-list-object-value')); - - hostElement.textContent = item.authority ? this.labelService.getHostLabel(item.scheme, item.authority) : localize('localAuthority', "Local"); - pathElement.textContent = item.scheme === Schemas.file ? URI.revive(item).fsPath : item.path; - - return rowElement; - } - - - protected renderEdit(item: IWorkspaceTrustUriDataItem, idx: number): HTMLElement { - const rowElement = $('.setting-list-edit-row'); - - const hostElement = append(rowElement, $('.setting-list-object-key')); - hostElement.textContent = item.authority ? this.labelService.getHostLabel(item.scheme, item.authority) : localize('localAuthority', "Local"); - - const updatedItem = () => { - if (item.scheme === Schemas.file) { - return URI.file(pathInput.value); - } else { - return URI.revive(item).with({ path: pathInput.value }); - } - }; - - const onKeyDown = (e: StandardKeyboardEvent) => { - if (e.equals(KeyCode.Enter)) { - this.handleItemChange(item, updatedItem(), idx); - } else if (e.equals(KeyCode.Escape)) { - this.cancelEdit(); - e.preventDefault(); - } - rowElement?.focus(); - }; - - const pathInput = new InputBox(rowElement, this.contextViewService, { - placeholder: this.getLocalizedStrings().inputPlaceholder - }); - - pathInput.element.classList.add('setting-list-valueInput'); - this.listDisposables.add(attachInputBoxStyler(pathInput, this.themeService, { - inputBackground: settingsSelectBackground, - inputForeground: settingsTextInputForeground, - inputBorder: settingsTextInputBorder - })); - this.listDisposables.add(pathInput); - pathInput.value = item.scheme === Schemas.file ? URI.revive(item).fsPath : item.path; - - this.listDisposables.add( - addStandardDisposableListener(pathInput.inputElement, EventType.KEY_DOWN, onKeyDown) - ); - - const okButton = this._register(new Button(rowElement)); - okButton.label = localize('okButton', "OK"); - okButton.element.classList.add('setting-list-ok-button'); - - this.listDisposables.add(attachButtonStyler(okButton, this.themeService)); - this.listDisposables.add(okButton.onDidClick(() => this.handleItemChange(item, updatedItem(), idx))); - - const cancelButton = this._register(new Button(rowElement)); - cancelButton.label = localize('cancelButton', "Cancel"); - cancelButton.element.classList.add('setting-list-cancel-button'); - - this.listDisposables.add(attachButtonStyler(cancelButton, this.themeService)); - this.listDisposables.add(cancelButton.onDidClick(() => this.cancelEdit())); - - this.listDisposables.add( - disposableTimeout(() => { - pathInput.focus(); - pathInput.select(); - }) - ); - - return rowElement; - } - - protected isItemNew(item: IWorkspaceTrustUriDataItem): boolean { - return item.path === ''; - } - - protected getLocalizedRowTitle(item: IWorkspaceTrustUriDataItem): string { - return localize('trustedRow', "Trusted Path: {0}", this.labelService.getUriLabel(URI.from(item))); - } - - protected getLocalizedStrings() { - return { - deleteActionTooltip: localize('removePath', "Remove Path"), - editActionTooltip: localize('editPath', "Edit Path"), - addButtonLabel: localize('addPath', "Add Path"), - hostHeaderText: localize('hostHeaderText', "Host"), - pathHeaderText: localize('pathHeaderText', "Path"), - inputPlaceholder: localize('pathInputPlaceholder', "Path Item..."), - }; - } -} - -interface IWorkspaceTrustSettingListItemTemplate extends IWorkspaceTrustSettingItemTemplate { - listWidget: WorkspaceTrustFolderSettingWidget; - validationErrorMessageElement: HTMLElement; -} - -export type WorkspaceTrustSettingListItemChangeType = 'added' | 'removed' | 'changed'; -export interface IWorkspaceTrustSettingChangeEvent { - key: string; - value: URI[] | undefined; // undefined => reset/unconfigure - type: WorkspaceTrustSettingListItemChangeType; -} - - -export class WorkspaceTrustSettingArrayRenderer extends Disposable implements ITreeRenderer { - templateId = 'template.setting.array'; - - static readonly CONTROL_CLASS = 'setting-control-focus-target'; - static readonly CONTROL_SELECTOR = '.' + WorkspaceTrustSettingArrayRenderer.CONTROL_CLASS; - static readonly CONTENTS_CLASS = 'setting-item-contents'; - static readonly CONTENTS_SELECTOR = '.' + WorkspaceTrustSettingArrayRenderer.CONTENTS_CLASS; - static readonly ALL_ROWS_SELECTOR = '.monaco-list-row'; - - static readonly SETTING_KEY_ATTR = 'data-key'; - static readonly SETTING_ID_ATTR = 'data-id'; - static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable'; - - protected readonly _onDidChangeSetting = this._register(new Emitter()); - readonly onDidChangeSetting: Event = this._onDidChangeSetting.event; - - private readonly _onDidFocusSetting = this._register(new Emitter()); - readonly onDidFocusSetting: Event = this._onDidFocusSetting.event; - - private readonly _onDidChangeIgnoredSettings = this._register(new Emitter()); - readonly onDidChangeIgnoredSettings: Event = this._onDidChangeIgnoredSettings.event; - - constructor( - @IThemeService protected readonly _themeService: IThemeService, - @IContextViewService protected readonly _contextViewService: IContextViewService, - @IOpenerService protected readonly _openerService: IOpenerService, - @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @ICommandService protected readonly _commandService: ICommandService, - @IContextMenuService protected readonly _contextMenuService: IContextMenuService, - @IKeybindingService protected readonly _keybindingService: IKeybindingService, - @IConfigurationService protected readonly _configService: IConfigurationService, - ) { - super(); - } - - renderCommonTemplate(tree: any, _container: HTMLElement, typeClass: string): IWorkspaceTrustSettingItemTemplate { - _container.classList.add('setting-item'); - _container.classList.add('setting-item-' + typeClass); - - const container = append(_container, $(WorkspaceTrustSettingArrayRenderer.CONTENTS_SELECTOR)); - container.classList.add('settings-row-inner-container'); - const titleElement = append(container, $('.setting-item-title')); - const labelCategoryContainer = append(titleElement, $('.setting-item-cat-label-container')); - const labelElement = append(labelCategoryContainer, $('span.setting-item-label')); - const descriptionElement = append(container, $('.setting-item-description')); - const modifiedIndicatorElement = append(container, $('.setting-item-modified-indicator')); - modifiedIndicatorElement.title = localize('modified', "Modified"); - - const valueElement = append(container, $('.setting-item-value')); - const controlElement = append(valueElement, $('div.setting-item-control')); - const toDispose = new DisposableStore(); - - const template: IWorkspaceTrustSettingItemTemplate = { - toDispose, - elementDisposables: new DisposableStore(), - containerElement: container, - labelElement, - descriptionElement, - controlElement - }; - - // Prevent clicks from being handled by list - toDispose.add(addDisposableListener(controlElement, EventType.MOUSE_DOWN, e => e.stopPropagation())); - - toDispose.add(addDisposableListener(titleElement, EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); - toDispose.add(addDisposableListener(titleElement, EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); - - return template; - } - - addSettingElementFocusHandler(template: IWorkspaceTrustSettingItemTemplate): void { - const focusTracker = trackFocus(template.containerElement); - template.toDispose.add(focusTracker); - focusTracker.onDidBlur(() => { - if (template.containerElement.classList.contains('focused')) { - template.containerElement.classList.remove('focused'); - } - }); - - focusTracker.onDidFocus(() => { - template.containerElement.classList.add('focused'); - - if (template.context) { - this._onDidFocusSetting.fire(template.context); - } - }); - } - - renderTemplate(container: HTMLElement): IWorkspaceTrustSettingListItemTemplate { - const common = this.renderCommonTemplate(null, container, 'list'); - const descriptionElement = common.containerElement.querySelector('.setting-item-description')!; - const validationErrorMessageElement = $('.setting-item-validation-message'); - descriptionElement.after(validationErrorMessageElement); - - const listWidget = this._instantiationService.createInstance(WorkspaceTrustFolderSettingWidget, common.controlElement); - listWidget.domNode.classList.add(WorkspaceTrustSettingArrayRenderer.CONTROL_CLASS); - common.toDispose.add(listWidget); - - const template: IWorkspaceTrustSettingListItemTemplate = { - ...common, - listWidget, - validationErrorMessageElement - }; - - this.addSettingElementFocusHandler(template); - - common.toDispose.add( - listWidget.onDidChangeList(e => { - const { list: newList, changeType } = this.computeNewList(template, e); - if (newList !== null && template.onChange) { - template.onChange(newList, changeType); - } - }) - ); - - return template; - } - - private computeNewList(template: IWorkspaceTrustSettingListItemTemplate, e: ISettingListChangeEvent): { list: URI[] | null, changeType: WorkspaceTrustSettingListItemChangeType } { - if (template.context) { - let newValue: URI[] = []; - - let changeType: WorkspaceTrustSettingListItemChangeType = 'changed'; - if (isArray(template.context.value)) { - newValue = [...template.context.value]; - } - - if (e.targetIndex !== undefined) { - // Delete value - if (!e.item?.path && e.originalItem.path && e.targetIndex > -1) { - newValue.splice(e.targetIndex, 1); - changeType = 'removed'; - } - // Update value - else if (e.item?.path && e.originalItem.path) { - if (e.targetIndex > -1) { - newValue[e.targetIndex] = URI.revive(e.item); - changeType = e.targetIndex < template.context.value.length ? 'changed' : 'added'; - } - // For some reason, we are updating and cannot find original value - // Just append the value in this case - else { - newValue.push(URI.revive(e.item)); - changeType = 'added'; - } - } - // Add value - else if (e.item?.path && !e.originalItem.path && e.targetIndex >= newValue.length) { - newValue.push(URI.revive(e.item)); - changeType = 'added'; - } - } - - return { list: newValue, changeType }; - } - - return { list: null, changeType: 'changed' }; - } - - renderElement(node: ITreeNode, index: number, template: IWorkspaceTrustSettingListItemTemplate): void { - const element = node.element; - template.context = element; - - template.containerElement.setAttribute(WorkspaceTrustSettingArrayRenderer.SETTING_KEY_ATTR, element.setting.key); - template.containerElement.setAttribute(WorkspaceTrustSettingArrayRenderer.SETTING_ID_ATTR, element.id); - - template.labelElement.textContent = element.displayLabel; - - template.descriptionElement.innerText = element.setting.description; - - const onChange = (value: any, type: WorkspaceTrustSettingListItemChangeType) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type }); - this.renderValue(element, template, onChange); - } - - protected renderValue(dataElement: WorkspaceTrustSettingsTreeEntry, template: IWorkspaceTrustSettingListItemTemplate, onChange: (value: URI[] | undefined, type: WorkspaceTrustSettingListItemChangeType) => void): void { - const value = getListDisplayValue(dataElement); - template.listWidget.setValue(value); - template.context = dataElement; - - template.onChange = (v, t) => { - onChange(v, t); - renderArrayValidations(dataElement, template, v, false); - }; - - renderArrayValidations(dataElement, template, value.map(v => URI.revive(v)), true); - } - - disposeTemplate(template: IWorkspaceTrustSettingItemTemplate): void { - dispose(template.toDispose); - } - - disposeElement(_element: ITreeNode, _index: number, template: IWorkspaceTrustSettingItemTemplate, _height: number | undefined): void { - if (template.elementDisposables) { - template.elementDisposables.clear(); - } - } -} - -export class WorkspaceTrustTree extends WorkbenchObjectTree { - constructor( - container: HTMLElement, - renderers: ITreeRenderer[], - @IContextKeyService contextKeyService: IContextKeyService, - @IListService listService: IListService, - @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super('WorkspaceTrustTree', container, - new WorkspaceTrustTreeDelegate(), - renderers, - { - horizontalScrolling: false, - alwaysConsumeMouseWheel: false, - supportDynamicHeights: true, - identityProvider: { - getId(e) { - return e.id; - } - }, - accessibilityProvider: new WorkspaceTrustTreeAccessibilityProvider(), - styleController: id => new DefaultStyleController(createStyleSheet(container), id), - smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), - multipleSelectionSupport: false, - }, - contextKeyService, - listService, - themeService, - configurationService, - keybindingService, - accessibilityService, - ); - - this.disposables.add(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const foregroundColor = theme.getColor(foreground); - if (foregroundColor) { - // Links appear inside other elements in markdown. CSS opacity acts like a mask. So we have to dynamically compute the description color to avoid - // applying an opacity to the link color. - const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.9)); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-description { color: ${fgWithOpacity}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .settings-toc-container .monaco-list-row:not(.selected) { color: ${fgWithOpacity}; }`); - - // Hack for subpixel antialiasing - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-title .setting-item-overrides, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-title .setting-item-ignored { color: ${fgWithOpacity}; }`); - } - - const errorColor = theme.getColor(errorForeground); - if (errorColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-deprecation-message { color: ${errorColor}; }`); - } - - const invalidInputBackground = theme.getColor(inputValidationErrorBackground); - if (invalidInputBackground) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { background-color: ${invalidInputBackground}; }`); - } - - const invalidInputForeground = theme.getColor(inputValidationErrorForeground); - if (invalidInputForeground) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { color: ${invalidInputForeground}; }`); - } - - const invalidInputBorder = theme.getColor(inputValidationErrorBorder); - if (invalidInputBorder) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-validation-message { border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item.invalid-input .setting-item-control .monaco-inputbox.idle { outline-width: 0; border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); - } - - const focusedRowBackgroundColor = theme.getColor(focusedRowBackground); - if (focusedRowBackgroundColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list-row.focused .settings-row-inner-container { background-color: ${focusedRowBackgroundColor}; }`); - } - - const focusedRowBorderColor = theme.getColor(focusedRowBorder); - if (focusedRowBorderColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::before, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::after { border-top: 1px solid ${focusedRowBorderColor} }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::before, - .workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::after { border-top: 1px solid ${focusedRowBorderColor} }`); - } - - const headerForegroundColor = theme.getColor(settingsHeaderForeground); - if (headerForegroundColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .settings-group-title-label { color: ${headerForegroundColor}; }`); - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-label { color: ${headerForegroundColor}; }`); - } - - const focusBorderColor = theme.getColor(focusBorder); - if (focusBorderColor) { - collector.addRule(`.workspace-trust-editor .workspace-trust-settings .workspace-trust-settings-tree-container .setting-item-contents .setting-item-markdown a:focus { outline-color: ${focusBorderColor} }`); - } - })); - - this.getHTMLElement().classList.add('settings-editor-tree'); - - this.disposables.add(attachStyler(themeService, { - listBackground: editorBackground, - listActiveSelectionBackground: editorBackground, - listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, - listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, - listFocusForeground: foreground, - listHoverForeground: foreground, - listHoverBackground: editorBackground, - listHoverOutline: editorBackground, - listFocusOutline: editorBackground, - listInactiveSelectionBackground: editorBackground, - listInactiveSelectionForeground: foreground, - listInactiveFocusBackground: editorBackground, - listInactiveFocusOutline: editorBackground - }, colors => { - this.style(colors); - })); - - this.disposables.add(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('workbench.list.smoothScrolling')) { - this.updateOptions({ - smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling') - }); - } - })); - } - - protected override createModel(user: string, view: IList>, options: IObjectTreeOptions): ITreeModel { - return new NonCollapsibleObjectTreeModel(user, view, options); - } -} - -export class WorkspaceTrustTreeModel { - - settings: WorkspaceTrustSettingsTreeEntry[] = []; - - update(trustedFolders: URI[]): void { - this.settings = []; - this.settings.push(new WorkspaceTrustSettingsTreeEntry( - 'trustedFolders', - localize('trustedFolders', "Trusted Folders"), - localize('trustedFoldersDescription', "You trust the following folders and their children: "), - trustedFolders)); - } -} - -class WorkspaceTrustTreeAccessibilityProvider implements IListAccessibilityProvider { - getAriaLabel(element: WorkspaceTrustSettingsTreeEntry) { - if (element instanceof WorkspaceTrustSettingsTreeEntry) { - return `element.displayLabel`; - } - - return null; - } - - getWidgetAriaLabel() { - return localize('settings', "Workspace Trust Setting"); - } -} - -class WorkspaceTrustTreeDelegate extends CachedListVirtualDelegate { - - getTemplateId(element: WorkspaceTrustSettingsTreeEntry): string { - return 'template.setting.array'; - } - - hasDynamicHeight(element: WorkspaceTrustSettingsTreeEntry): boolean { - return true; - } - - protected estimateHeight(element: WorkspaceTrustSettingsTreeEntry): number { - return 104; - } -} - -function getListDisplayValue(element: WorkspaceTrustSettingsTreeEntry): IWorkspaceTrustUriDataItem[] { - if (!element.value || !isArray(element.value)) { - return []; - } - - return element.value; -} - -function renderArrayValidations(dataElement: WorkspaceTrustSettingsTreeEntry, template: IWorkspaceTrustSettingListItemTemplate, v: URI[] | undefined, arg3: boolean) { -} - From 8f1408281072b352a0b90c3b62d7cce1a3f8632f Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 May 2021 17:09:22 -0700 Subject: [PATCH 067/270] fix mouse click double background --- .../contrib/notebook/browser/media/notebook.css | 9 +++++++++ .../notebook/browser/notebookEditorToolbar.ts | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 364c9c27432..64f51120dc9 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -69,6 +69,15 @@ background-size: 16px; padding: 0px 5px 0px 2px; border-radius: 5px; + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active .action-label:not(.disabled) { + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { + background-color: unset; } .monaco-workbench .cell.markdown { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts index ecddc6d68cd..3fc2773c8bb 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts @@ -16,6 +16,8 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { toolbarActiveBackground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { INotebookEditor, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem'; @@ -214,3 +216,14 @@ export class NotebookEditorToolbar extends Disposable { super.dispose(); } } + +registerThemingParticipant((theme, collector) => { + const toolbarActiveBackgroundColor = theme.getColor(toolbarActiveBackground); + if (toolbarActiveBackgroundColor) { + collector.addRule(` + .monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { + background-color: ${toolbarActiveBackgroundColor}; + } + `); + } +}); From bbb89b86f567afa68bf1d8152723f81e32f40bdc Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 18 May 2021 17:19:06 -0700 Subject: [PATCH 068/270] remove hover bg color on notebook toolbar icon. --- src/vs/workbench/contrib/notebook/browser/media/notebook.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 64f51120dc9..33bfd31588f 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -76,6 +76,10 @@ background-color: unset; } +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-label:not(.disabled):hover { + background-color: unset; +} + .monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { background-color: unset; } From 721cdd6472b25ae5fd70a36f485bcb26beb0c96e Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Tue, 18 May 2021 21:16:00 -0400 Subject: [PATCH 069/270] Fix data loss when renaming custom editors (#124057) * Start structuring the custom editor rename work * Restore renamed / moved editors from backup * Add back missing readonly * Discard backup after using it * Address feedback * Remove console log * Switch to using a map * Delete used backups from the map --- .../api/browser/mainThreadCustomEditors.ts | 47 +++++++++++++++++-- .../customEditor/browser/customEditorInput.ts | 10 ++-- .../customEditor/common/customEditor.ts | 2 + .../common/customEditorModelManager.ts | 10 ++++ 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 66a82a4dade..c860e4ea7fd 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -16,7 +16,7 @@ import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resour import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; +import { FileOperation, FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; @@ -35,7 +35,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy, IWorkingCopyBackup, NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; @@ -51,6 +51,8 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc private readonly _editorProviders = new Map(); + private readonly _editorRenameBackups = new Map(); + constructor( context: extHostProtocol.IExtHostContext, private readonly mainThreadWebview: MainThreadWebviews, @@ -61,7 +63,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -90,6 +92,9 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc }, resolveWebview: () => { throw new Error('not implemented'); } })); + + // Working copy operations + this._register(workingCopyFileService.onWillRunWorkingCopyFileOperation(async e => this.onWillRunWorkingCopyFileOperation(e))); } override dispose() { @@ -138,9 +143,18 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc webviewInput.webview.options = options; webviewInput.webview.extension = extension; + // If there's an old resource this was a move and we must resolve the backup at the same time as the webview + // This is because the backup must be ready upon model creation, and the input resolve method comes after + let backupId = webviewInput.backupId; + if (webviewInput.oldResource && !webviewInput.backupId) { + const backup = this._editorRenameBackups.get(webviewInput.oldResource.toString()); + backupId = backup?.backupId; + this._editorRenameBackups.delete(webviewInput.oldResource.toString()); + } + let modelRef: IReference; try { - modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation); + modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId }, cancellation); } catch (error) { onUnexpectedError(error); webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); @@ -253,6 +267,31 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc } return model; } + + //#region Working Copy + private async onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent) { + if (e.operation !== FileOperation.MOVE) { + return; + } + e.waitUntil((async () => { + const models = []; + for (const file of e.files) { + if (file.source) { + models.push(...(await this._customEditorService.models.getAllModels(file.source))); + } + } + for (const model of models) { + if (model instanceof MainThreadCustomEditorModel && model.isDirty()) { + const workingCopy = await model.backup(CancellationToken.None); + if (workingCopy.meta) { + // This cast is safe because we do an instanceof check above and a custom document backup data is always returned + this._editorRenameBackups.set(model.editorResource.toString(), workingCopy.meta as CustomDocumentBackupData); + } + } + } + })()); + } + //#endregion } namespace HotExitState { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 7334c3f1d34..e19caa20fc8 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -32,7 +32,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { resource: URI, viewType: string, group: GroupIdentifier | undefined, - options?: { readonly customClasses?: string }, + options?: { readonly customClasses?: string, readonly oldResource?: URI }, ): IEditorInput { return instantiationService.invokeFunction(accessor => { if (viewType === defaultCustomEditor.id) { @@ -43,7 +43,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { let untitledDocumentData = untitledString ? VSBuffer.fromString(untitledString) : undefined; const id = generateUuid(); const webview = accessor.get(IWebviewService).createWebviewOverlay(id, { customClasses: options?.customClasses }, {}, undefined); - const input = instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, { untitledDocumentData: untitledDocumentData }); + const input = instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, { untitledDocumentData: untitledDocumentData, oldResource: options?.oldResource }); if (typeof group !== 'undefined') { input.updateGroup(group); } @@ -54,6 +54,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { public static override readonly typeId = 'workbench.editors.webviewEditor'; private readonly _editorResource: URI; + public readonly oldResource?: URI; private _defaultDirtyState: boolean | undefined; private readonly _backupId: string | undefined; @@ -69,7 +70,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { viewType: string, id: string, webview: WebviewOverlay, - options: { startsDirty?: boolean, backupId?: string, untitledDocumentData?: VSBuffer }, + options: { startsDirty?: boolean, backupId?: string, untitledDocumentData?: VSBuffer, readonly oldResource?: URI }, @IWebviewWorkbenchService webviewWorkbenchService: IWebviewWorkbenchService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, @@ -80,6 +81,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { ) { super(id, viewType, '', webview, webviewWorkbenchService); this._editorResource = resource; + this.oldResource = options.oldResource; this._defaultDirtyState = options.startsDirty; this._backupId = options.backupId; this._untitledDocumentData = options.untitledDocumentData; @@ -241,7 +243,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { private doMove(group: GroupIdentifier, newResource: URI): IEditorInput { if (!this._moveHandler) { - return CustomEditorInput.create(this.instantiationService, newResource, this.viewType, group); + return CustomEditorInput.create(this.instantiationService, newResource, this.viewType, group, { oldResource: this.resource }); } this._moveHandler(newResource); diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index c7aa1260e3f..bfaa0a181f8 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -41,6 +41,8 @@ export interface ICustomEditorService { } export interface ICustomEditorModelManager { + getAllModels(resource: URI): Promise + get(resource: URI, viewType: string): Promise; tryRetain(resource: URI, viewType: string): Promise> | undefined; diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts index b2b185ed874..0d8176fadc4 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -16,6 +16,16 @@ export class CustomEditorModelManager implements ICustomEditorModelManager { counter: number }>(); + public async getAllModels(resource: URI): Promise { + const keyStart = `${resource.toString()}@@@`; + const models = []; + for (const [key, entry] of this._references) { + if (key.startsWith(keyStart) && entry.model) { + models.push(await entry.model); + } + } + return models; + } public async get(resource: URI, viewType: string): Promise { const key = this.key(resource, viewType); const entry = this._references.get(key); From 97518f5e869969ff66b7146175878adf34b85037 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 18 May 2021 21:53:57 -0700 Subject: [PATCH 070/270] Copy testRepeat helper to extension integration tests --- extensions/vscode-api-tests/src/utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index a36b7549626..83dc7aa4558 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -137,3 +137,9 @@ export async function asPromise(event: vscode.Event, timeout = vscode.env. }); }); } + +export function testRepeat(n: number, description: string, callback: (this: any) => any): void { + for (let i = 0; i < n; i++) { + test(`${description} (iteration ${i})`, callback); + } +} From 32d4df9b823b27f3f8735402af9990c5b555698c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 07:35:54 +0200 Subject: [PATCH 071/270] untitled file working copy - shared dispose handling --- .../common/abstractFileWorkingCopy.ts | 2 +- .../common/abstractFileWorkingCopyManager.ts | 24 ++++++++++ .../common/fileWorkingCopyManager.ts | 30 +------------ .../common/untitledFileWorkingCopyManager.ts | 45 +++++++++++++------ 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts index aa49c394e09..3568f1ca66b 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts @@ -24,7 +24,7 @@ export interface IBaseFileWorkingCopyModelFactory> extends Disposable implements IBaseFileWorkingCopyManager { private readonly mapResourceToWorkingCopy = new ResourceMap(); + private readonly mapResourceToDisposeListener = new ResourceMap(); constructor( protected readonly workingCopyTypeId: string, @@ -111,10 +112,29 @@ export abstract class BaseFileWorkingCopyManager this.remove(resource))); } protected remove(resource: URI): void { + + // Dispose any existing listener + const disposeListener = this.mapResourceToDisposeListener.get(resource); + if (disposeListener) { + dispose(disposeListener); + this.mapResourceToDisposeListener.delete(resource); + } + + // Remove from our working copy map this.mapResourceToWorkingCopy.delete(resource); } @@ -330,6 +350,10 @@ export abstract class BaseFileWorkingCopyManager { diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index ebbbb29a82e..309f5e80655 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -162,7 +162,6 @@ export class FileWorkingCopyManager extends Bas //#endregion private readonly mapResourceToWorkingCopyListeners = new ResourceMap(); - private readonly mapResourceToDisposeListener = new ResourceMap(); private readonly mapResourceToPendingWorkingCopyResolve = new ResourceMap>(); private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); @@ -515,32 +514,10 @@ export class FileWorkingCopyManager extends Bas this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); } - protected override add(resource: URI, workingCopy: IFileWorkingCopy): void { - const knownWorkingCopy = this.get(resource); - if (knownWorkingCopy === workingCopy) { - return; // already cached - } - - // Dispose any previously stored dispose listener for this resource - const disposeListener = this.mapResourceToDisposeListener.get(resource); - if (disposeListener) { - disposeListener.dispose(); - } - - // Store in cache but remove when working copy gets disposed - super.add(resource, workingCopy); - this.mapResourceToDisposeListener.set(resource, workingCopy.onWillDispose(() => this.remove(resource))); - } - protected override remove(resource: URI): void { super.remove(resource); - const disposeListener = this.mapResourceToDisposeListener.get(resource); - if (disposeListener) { - dispose(disposeListener); - this.mapResourceToDisposeListener.delete(resource); - } - + // Dispose any exsting working copy listeners const workingCopyListener = this.mapResourceToWorkingCopyListeners.get(resource); if (workingCopyListener) { dispose(workingCopyListener); @@ -591,12 +568,9 @@ export class FileWorkingCopyManager extends Bas override dispose(): void { super.dispose(); + // Clear pending working copy resolves this.mapResourceToPendingWorkingCopyResolve.clear(); - // Dispose the dispose listeners - dispose(this.mapResourceToDisposeListener.values()); - this.mapResourceToDisposeListener.clear(); - // Dispose the working copy change listeners dispose(this.mapResourceToWorkingCopyListeners.values()); this.mapResourceToWorkingCopyListeners.clear(); diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 72f230c634d..28ee5986867 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { Event, Emitter } from 'vs/base/common/event'; @@ -22,6 +22,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { ResourceMap } from 'vs/base/common/map'; /** * The only one that should be dealing with `IUntitledFileWorkingCopy` and @@ -102,6 +103,8 @@ export class UntitledFileWorkingCopyManager(); + constructor( workingCopyTypeId: string, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, @@ -223,19 +226,12 @@ export class UntitledFileWorkingCopyManager): void { // Install working copy listeners - const listeners = new DisposableStore(); - listeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); - listeners.add(workingCopy.onWillDispose(() => this._onWillDispose.fire(workingCopy))); + const workingCopyListeners = new DisposableStore(); + workingCopyListeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); + workingCopyListeners.add(workingCopy.onWillDispose(() => this._onWillDispose.fire(workingCopy))); - // Remove from cache on dispose - Event.once(workingCopy.onWillDispose)(() => { - - // Registry - this.remove(workingCopy.resource); - - // Listeners - listeners.dispose(); - }); + // Keep for disposal + this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); // Add to cache this.add(workingCopy.resource, workingCopy); @@ -247,5 +243,28 @@ export class UntitledFileWorkingCopyManager Date: Wed, 19 May 2021 09:00:39 +0200 Subject: [PATCH 072/270] untitled file working copy - add new manager that unifies file and untitled working copies --- .../common/abstractFileWorkingCopyManager.ts | 244 +----------- .../common/fileWorkingCopyManager.ts | 144 +++++-- .../common/fileWorkingCopyManager2.ts | 353 ++++++++++++++++++ .../common/untitledFileWorkingCopy.ts | 6 +- .../common/untitledFileWorkingCopyManager.ts | 43 +-- .../browser/fileWorkingCopyManager.test.ts | 16 +- .../untitledFileWorkingCopyManager.test.ts | 151 ++------ 7 files changed, 512 insertions(+), 445 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 19ffab8a89b..0659e102304 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { Promises } from 'vs/base/common/async'; @@ -12,20 +11,6 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; -import { FileWorkingCopy, IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; -import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; -import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; -import { IConfirmation, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { basename, dirname, isEqual, joinPath, toLocalResource } from 'vs/base/common/resources'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { isValidBasename } from 'vs/base/common/extpath'; -import { ISaveOptions } from 'vs/workbench/common/editor'; export interface IBaseFileWorkingCopyManager> extends IDisposable { @@ -40,29 +25,6 @@ export interface IBaseFileWorkingCopyManager | undefined>; - saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined>; - /** * Disposes all working copies of the manager and disposes the manager. This * method is different from `dispose` in that it will unregister any working @@ -76,33 +38,15 @@ export interface IBaseFileWorkingCopyManager; } -export interface IBaseFileWorkingCopySaveAsOptions extends ISaveOptions { - - /** - * Optional target resource to suggest to the user in case - * no taget resource is provided to save to. - */ - suggestedTarget?: URI; -} - export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { private readonly mapResourceToWorkingCopy = new ResourceMap(); private readonly mapResourceToDisposeListener = new ResourceMap(); constructor( - protected readonly workingCopyTypeId: string, - private readonly fileWorkingCopyResolver: IFileWorkingCopyResolver, @IFileService protected readonly fileService: IFileService, @ILogService protected readonly logService: ILogService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, - @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, - @IWorkingCopyFileService protected readonly workingCopyFileService: IWorkingCopyFileService, - @IDialogService private readonly dialogService: IDialogService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IPathService private readonly pathService: IPathService + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService ) { super(); } @@ -148,192 +92,6 @@ export abstract class BaseFileWorkingCopyManager | undefined { - const workingCopy = this.workingCopyService.get({ resource, typeId: this.workingCopyTypeId }); - if (workingCopy instanceof FileWorkingCopy) { - return workingCopy; - } - - return undefined; - } - - //#endregion - - //#region Save - - async saveAs(source: URI, target?: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined> { - - // Get to target resource - if (!target) { - const workingCopy = this.get(source); - if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { - target = await this.suggestSavePath(source); - } else { - target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); - } - } - - if (!target) { - return; // user canceled - } - - // Just save if target is same as working copies own resource - // and we are not saving an untitled file working copy - if (this.fileService.canHandleResource(source) && isEqual(source, target)) { - return this.doSave(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); - } - - // If the target is different but of same identity, we - // move the source to the target, knowing that the - // underlying file system cannot have both and then save. - // However, this will only work if the source exists - // and is not orphaned, so we need to check that too. - if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) { - - // Move via working copy file service to enable participants - await this.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); - - // At this point we don't know whether we have a - // working copy for the source or the target URI so we - // simply try to save with both resources. - return (await this.doSave(source, options)) ?? (await this.doSave(target, options)); - } - - // Perform normal "Save As" - return this.doSaveAs(source, target, options); - } - - private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { - - // Save is only possible with file working copies, - // any other have to go via `saveAs` flow. - const fileWorkingCopy = this.getFile(resource); - if (fileWorkingCopy) { - const success = await fileWorkingCopy.save(options); - if (success) { - return fileWorkingCopy; - } - } - - return undefined; - } - - private async doSaveAs(source: URI, target: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined> { - let sourceContents: VSBufferReadableStream; - - // If the source is an existing file working copy, we can directly - // use that to copy the contents to the target destination - const sourceWorkingCopy = this.get(source); - if (sourceWorkingCopy?.isResolved()) { - sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); - } - - // Otherwise we resolve the contents from the underlying file - else { - sourceContents = (await this.fileService.readFileStream(source)).value; - } - - // Resolve target - const { targetFileExists, targetFileWorkingCopy } = await this.doResolveSaveTarget(source, target); - - // Confirm to overwrite if we have an untitled file working copy with associated path where - // the file actually exists on disk and we are instructed to save to that file path. - // This can happen if the file was created after the untitled file was opened. - // See https://github.com/microsoft/vscode/issues/67946 - if ( - sourceWorkingCopy instanceof UntitledFileWorkingCopy && - sourceWorkingCopy.hasAssociatedFilePath && - targetFileExists && - this.uriIdentityService.extUri.isEqual(target, toLocalResource(sourceWorkingCopy.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme)) - ) { - const overwrite = await this.confirmOverwrite(target); - if (!overwrite) { - return undefined; - } - } - - // Take over content from source to target - await targetFileWorkingCopy.model?.update(sourceContents, CancellationToken.None); - - // Save target - await targetFileWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); - - // Revert the source - await sourceWorkingCopy?.revert(); - - return targetFileWorkingCopy; - } - - private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetFileWorkingCopy: IFileWorkingCopy }> { - - // Prefer an existing file working copy if it is already resolved - // for the given target resource - let targetFileExists = false; - let targetFileWorkingCopy = this.getFile(target); - if (targetFileWorkingCopy?.isResolved()) { - targetFileExists = true; - } - - // Otherwise create the target working copy empty if - // it does not exist already and resolve it from there - else { - targetFileExists = await this.fileService.exists(target); - - // Create target file adhoc if it does not exist yet - if (!targetFileExists) { - await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); - } - - // At this point we need to resolve the target working copy - // and we have to do an explicit check if the source URI - // equals the target via URI identity. If they match and we - // have had an existing working copy with the source, we - // prefer that one over resolving the target. Otherwise we - // would potentially introduce a - if (this.uriIdentityService.extUri.isEqual(source, target) && this.has(source)) { - targetFileWorkingCopy = await this.fileWorkingCopyResolver(source); - } else { - targetFileWorkingCopy = await this.fileWorkingCopyResolver(target); - } - } - - return { targetFileExists, targetFileWorkingCopy }; - } - - private async confirmOverwrite(resource: URI): Promise { - const confirm: IConfirmation = { - message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), - detail: localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))), - primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - - const result = await this.dialogService.confirm(confirm); - return result.confirmed; - } - - private async suggestSavePath(resource: URI): Promise { - - // 1.) Just take the resource as is if the file service can handle it - if (this.fileService.canHandleResource(resource)) { - return resource; - } - - // 2.) Pick the associated file path for untitled working copies if any - const workingCopy = this.get(resource); - if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { - return toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); - } - - // 3.) Pick the working copy name if valid joined with default path - if (workingCopy && isValidBasename(workingCopy.name)) { - return joinPath(await this.fileDialogService.defaultFilePath(), workingCopy.name); - } - - // 4.) Finally fallback to the name of the resource joined with default path - return joinPath(await this.fileDialogService.defaultFilePath(), basename(resource)); - } - //#endregion //#region Lifecycle diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 309f5e80655..f1c7354fa8b 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -16,16 +16,13 @@ import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { joinPath } from 'vs/base/common/resources'; import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; /** * The only one that should be dealing with `IFileWorkingCopy` and handle all @@ -89,6 +86,13 @@ export interface IFileWorkingCopyManager extend * it is dirty. Once the promise is settled, it is safe to dispose. */ canDispose(workingCopy: IFileWorkingCopy): true | Promise; + + + /** + * @deprecated + */ + saveAs(source: URI, target: URI): Promise | undefined>; + saveAs(source: URI, target: undefined): Promise | undefined>; } export interface IFileWorkingCopySaveEvent { @@ -129,14 +133,6 @@ export interface IFileWorkingCopyResolveOptions { }; } -export interface IFileWorkingCopyResolver { - - /** - * A delegate to resolve a file working copy. - */ - (resource: URI): Promise>; -} - export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events @@ -167,36 +163,19 @@ export class FileWorkingCopyManager extends Bas private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); constructor( - workingCopyTypeId: string, + private readonly workingCopyTypeId: string, private readonly modelFactory: IFileWorkingCopyModelFactory, @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, - @IFileDialogService fileDialogService: IFileDialogService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, - @IUriIdentityService uriIdentityService: IUriIdentityService, - @IDialogService dialogService: IDialogService, - @IWorkingCopyService workingCopyService: IWorkingCopyService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IPathService pathService: IPathService + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IFileDialogService private readonly fileDialogService: IFileDialogService ) { - super( - workingCopyTypeId, - resource => this.resolve(resource), - fileService, - logService, - workingCopyBackupService, - fileDialogService, - uriIdentityService, - workingCopyFileService, - dialogService, - workingCopyService, - environmentService, - pathService - ); + super(fileService, logService, workingCopyBackupService); this.registerListeners(); } @@ -577,4 +556,101 @@ export class FileWorkingCopyManager extends Bas } //#endregion + + + //#region Save As... + + /** + * @deprecated TODO@bpasero remove + */ + async saveAs(source: URI, target?: URI): Promise | undefined> { + + // If not provided, ask user for target + if (!target) { + target = await this.fileDialogService.pickFileToSave(source); + + if (!target) { + return undefined; // user canceled + } + } + + // Do it + return this.doSaveAs(source, target); + } + + private async doSaveAs(source: URI, target: URI): Promise | undefined> { + let sourceContents: VSBufferReadableStream; + + // If the source is an existing file working copy, we can directly + // use that to copy the contents to the target destination + const sourceWorkingCopy = this.get(source); + if (sourceWorkingCopy?.isResolved()) { + sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); + } + + // Otherwise we resolve the contents from the underlying file + else { + sourceContents = (await this.fileService.readFileStream(source)).value; + } + + // Save the contents through working copy to benefit from save + // participants and handling a potential already existing target + return this.doSaveAsWorkingCopy(source, sourceContents, target); + } + + private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI): Promise> { + + // Prefer an existing working copy if it is already resolved + // for the given target resource + let targetExists = false; + let targetWorkingCopy = this.get(target); + if (targetWorkingCopy?.isResolved()) { + targetExists = true; + } + + // Otherwise create the target working copy empty if + // it does not exist already and resolve it from there + else { + targetExists = await this.fileService.exists(target); + + // Create target file adhoc if it does not exist yet + if (!targetExists) { + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); + } + + // At this point we need to resolve the target working copy + // and we have to do an explicit check if the source URI + // equals the target via URI identity. If they match and we + // have had an existing working copy with the source, we + // prefer that one over resolving the target. Otherwiese we + // would potentially introduce a + if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { + targetWorkingCopy = await this.resolve(source); + } else { + targetWorkingCopy = await this.resolve(target); + } + } + + // Take over content from source to target + await targetWorkingCopy.model?.update(sourceContents, CancellationToken.None); + + // Save target + await targetWorkingCopy.save({ force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + + // Revert the source + await this.doRevert(source); + + return targetWorkingCopy; + } + + private async doRevert(resource: URI): Promise { + const workingCopy = this.get(resource); + if (!workingCopy) { + return undefined; + } + + return workingCopy.revert(); + } + + //#endregion } diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts new file mode 100644 index 00000000000..b6c1944af3a --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts @@ -0,0 +1,353 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { Promises } from 'vs/base/common/async'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { toLocalResource, joinPath, isEqual, basename, dirname } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { IFileDialogService, IDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ISaveOptions } from 'vs/workbench/common/editor'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { IExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyWithAssociatedResourceOptions, IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { isValidBasename } from 'vs/base/common/extpath'; + +export interface IFileWorkingCopyManager2 extends IDisposable { + + /** + * Provides access to the manager for titled file working copies. + */ + readonly files: IFileWorkingCopyManager; + + /** + * Provides access to the manager for untitled file working copies. + */ + readonly untitled: IUntitledFileWorkingCopyManager; + + /** + * Resolves an untitled file working copy from the provided options. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Resolves an untitled file working copy from the provided options + * unless an existing working copy already exists with that resource. + */ + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + + /** + * Allows to resolve a file working copy. If the manager already knows + * about a file working copy with the same `URI`, it will return that + * existing file working copy. There will never be more than one + * file working copy per `URI` until the file working copy is disposed. + * + * Use the `IFileWorkingCopyResolveOptions.reload` option to control the + * behaviour for when a file working copy was previously already resolved + * with regards to resolving it again from the underlying file resource + * or not. + * + * Note: Callers must `dispose` the working copy when no longer needed. + * + * @param resource used as unique identifier of the file working copy in + * case one is already known for this `URI`. + * @param options + */ + resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; + + /** + * Implements "Save As" for file based working copies. The API is `URI` based + * because it works even without resolved file working copies. If a file working + * copy exists for any given `URI`, the implementation will deal with them properly + * (e.g. dirty contents of the source will be written to the target and the source + * will be reverted). + * + * Note: it is possible that the returned file working copy has a different `URI` + * than the `target` that was passed in. Based on URI identity, the file working + * copy may chose to return an existing file working copy with different casing + * to respect file systems that are case insensitive. + * + * Note: Callers must `dispose` the working copy when no longer needed. + * + * @param source the source resource to save as + * @param target the optional target resource to save to. if not defined, the user + * will be asked for input + * @returns the target working copy that was saved to or `undefined` in case of + * cancellation + */ + saveAs(source: URI, target: URI, options?: ISaveOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; + + /** + * Disposes all working copies of the manager and disposes the manager. This + * method is different from `dispose` in that it will unregister any working + * copy from the `IWorkingCopyService`. Since this impact things like backups, + * the method is `async` because it needs to trigger `save` for any dirty + * working copy to preserve the data. + * + * Callers should make sure to e.g. close any editors associated with the + * working copy. + */ + destroy(): Promise; +} + +export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { + + /** + * Optional target resource to suggest to the user in case + * no taget resource is provided to save to. + */ + suggestedTarget?: URI; +} + +export class FileWorkingCopyManager2 extends Disposable implements IFileWorkingCopyManager2 { + + readonly files: IFileWorkingCopyManager; + readonly untitled: IUntitledFileWorkingCopyManager; + + constructor( + private readonly workingCopyTypeId: string, + private readonly fileModelFactory: IFileWorkingCopyModelFactory, + private readonly untitledFileModelFactory: IUntitledFileWorkingCopyModelFactory, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IDialogService private readonly dialogService: IDialogService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IPathService private readonly pathService: IPathService + ) { + super(); + + // File manager + this.files = this._register(this.instantiationService.createInstance(FileWorkingCopyManager, this.workingCopyTypeId, this.fileModelFactory)) as unknown as IFileWorkingCopyManager; + + // Untitled manager + this.untitled = this._register(this.instantiationService.createInstance(UntitledFileWorkingCopyManager, this.workingCopyTypeId, this.untitledFileModelFactory, async (workingCopy, options) => { + const result = await this.saveAs(workingCopy.resource, undefined, options); + + return result ? true : false; + })) as unknown as IUntitledFileWorkingCopyManager; + } + + //#region resolve + + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; + resolve(arg1?: URI | INewUntitledFileWorkingCopyOptions | INewUntitledFileWorkingCopyWithAssociatedResourceOptions | IExistingUntitledFileWorkingCopyOptions, arg2?: IFileWorkingCopyResolveOptions): Promise | IFileWorkingCopy> { + if (URI.isUri(arg1)) { + return this.files.resolve(arg1, arg2); + } + + return this.untitled.resolve(arg1); + } + + //#endregion + + //#region Save + + async saveAs(source: URI, target?: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { + + // Get to target resource + if (!target) { + const workingCopy = this.get(source); + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + target = await this.suggestSavePath(source); + } else { + target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + } + } + + if (!target) { + return; // user canceled + } + + // Just save if target is same as working copies own resource + // and we are not saving an untitled file working copy + if (this.fileService.canHandleResource(source) && isEqual(source, target)) { + return this.doSave(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + } + + // If the target is different but of same identity, we + // move the source to the target, knowing that the + // underlying file system cannot have both and then save. + // However, this will only work if the source exists + // and is not orphaned, so we need to check that too. + if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) { + + // Move via working copy file service to enable participants + await this.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); + + // At this point we don't know whether we have a + // working copy for the source or the target URI so we + // simply try to save with both resources. + return (await this.doSave(source, options)) ?? (await this.doSave(target, options)); + } + + // Perform normal "Save As" + return this.doSaveAs(source, target, options); + } + + private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { + + // Save is only possible with file working copies, + // any other have to go via `saveAs` flow. + const fileWorkingCopy = this.files.get(resource); + if (fileWorkingCopy) { + const success = await fileWorkingCopy.save(options); + if (success) { + return fileWorkingCopy; + } + } + + return undefined; + } + + private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { + let sourceContents: VSBufferReadableStream; + + // If the source is an existing file working copy, we can directly + // use that to copy the contents to the target destination + const sourceWorkingCopy = this.get(source); + if (sourceWorkingCopy?.isResolved()) { + sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); + } + + // Otherwise we resolve the contents from the underlying file + else { + sourceContents = (await this.fileService.readFileStream(source)).value; + } + + // Resolve target + const { targetFileExists, targetFileWorkingCopy } = await this.doResolveSaveTarget(source, target); + + // Confirm to overwrite if we have an untitled file working copy with associated path where + // the file actually exists on disk and we are instructed to save to that file path. + // This can happen if the file was created after the untitled file was opened. + // See https://github.com/microsoft/vscode/issues/67946 + if ( + sourceWorkingCopy instanceof UntitledFileWorkingCopy && + sourceWorkingCopy.hasAssociatedFilePath && + targetFileExists && + this.uriIdentityService.extUri.isEqual(target, toLocalResource(sourceWorkingCopy.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme)) + ) { + const overwrite = await this.confirmOverwrite(target); + if (!overwrite) { + return undefined; + } + } + + // Take over content from source to target + await targetFileWorkingCopy.model?.update(sourceContents, CancellationToken.None); + + // Save target + await targetFileWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + + // Revert the source + await sourceWorkingCopy?.revert(); + + return targetFileWorkingCopy; + } + + private get(resource: URI): IUntitledFileWorkingCopy | IFileWorkingCopy | undefined { + return this.files.get(resource) ?? this.untitled.get(resource); + } + + private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetFileWorkingCopy: IFileWorkingCopy }> { + + // Prefer an existing file working copy if it is already resolved + // for the given target resource + let targetFileExists = false; + let targetFileWorkingCopy = this.files.get(target); + if (targetFileWorkingCopy?.isResolved()) { + targetFileExists = true; + } + + // Otherwise create the target working copy empty if + // it does not exist already and resolve it from there + else { + targetFileExists = await this.fileService.exists(target); + + // Create target file adhoc if it does not exist yet + if (!targetFileExists) { + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); + } + + // At this point we need to resolve the target working copy + // and we have to do an explicit check if the source URI + // equals the target via URI identity. If they match and we + // have had an existing working copy with the source, we + // prefer that one over resolving the target. Otherwise we + // would potentially introduce a + if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { + targetFileWorkingCopy = await this.files.resolve(source); + } else { + targetFileWorkingCopy = await this.files.resolve(target); + } + } + + return { targetFileExists, targetFileWorkingCopy }; + } + + private async confirmOverwrite(resource: URI): Promise { + const confirm: IConfirmation = { + message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), + detail: localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + const result = await this.dialogService.confirm(confirm); + return result.confirmed; + } + + private async suggestSavePath(resource: URI): Promise { + + // 1.) Just take the resource as is if the file service can handle it + if (this.fileService.canHandleResource(resource)) { + return resource; + } + + // 2.) Pick the associated file path for untitled working copies if any + const workingCopy = this.get(resource); + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + return toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); + } + + // 3.) Pick the working copy name if valid joined with default path + if (workingCopy && isValidBasename(workingCopy.name)) { + return joinPath(await this.fileDialogService.defaultFilePath(), workingCopy.name); + } + + // 4.) Finally fallback to the name of the resource joined with default path + return joinPath(await this.fileDialogService.defaultFilePath(), basename(resource)); + } + + //#endregion + + //#region Lifecycle + + async destroy(): Promise { + await Promises.settled([ + this.files.destroy(), + this.untitled.destroy() + ]); + } + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 536517a7431..90d24089bbb 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -66,12 +66,12 @@ export interface IResolvedUntitledFileWorkingCopy { +export interface IUntitledFileWorkingCopySaveDelegate { /** * A delegate to enable saving of untitled file working copies. */ - (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; + (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; } export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { @@ -104,7 +104,7 @@ export class UntitledFileWorkingCopy ex readonly hasAssociatedFilePath: boolean, private readonly initialValue: VSBufferReadableStream | undefined, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, - private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @ILogService private readonly logService: ILogService diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 28ee5986867..09562661f3d 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -6,7 +6,7 @@ import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, IUntitledFileWorkingCopySaveDelegate, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { Event, Emitter } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,13 +15,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService } from 'vs/platform/files/common/files'; import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; -import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { ResourceMap } from 'vs/base/common/map'; /** @@ -106,36 +99,16 @@ export class UntitledFileWorkingCopyManager(); constructor( - workingCopyTypeId: string, + private readonly workingCopyTypeId: string, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, - fileWorkingCopyResolver: IFileWorkingCopyResolver, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IFileService fileService: IFileService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, - @IFileDialogService fileDialogService: IFileDialogService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, - @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, - @IUriIdentityService uriIdentityService: IUriIdentityService, - @IDialogService dialogService: IDialogService, - @IWorkingCopyService workingCopyService: IWorkingCopyService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IPathService pathService: IPathService + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService ) { - super( - workingCopyTypeId, - resource => fileWorkingCopyResolver(resource), - fileService, - logService, - workingCopyBackupService, - fileDialogService, - uriIdentityService, - workingCopyFileService, - dialogService, - workingCopyService, - environmentService, - pathService - ); + super(fileService, logService, workingCopyBackupService); } //#region Resolve @@ -211,11 +184,7 @@ export class UntitledFileWorkingCopyManager (async () => { - const result = await this.saveAs(workingCopy.resource, undefined, options); - - return result ? true : false; - })() + this.saveDelegate ) as unknown as IUntitledFileWorkingCopy; this.registerWorkingCopy(workingCopy); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index cbc543d430a..9905e3ac630 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -34,14 +34,10 @@ suite('FileWorkingCopyManager', () => { accessor.labelService, instantiationService, accessor.logService, - accessor.fileDialogService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, - accessor.dialogService, - accessor.workingCopyService, - accessor.environmentService, - accessor.pathService + accessor.fileDialogService ); }); @@ -542,15 +538,7 @@ suite('FileWorkingCopyManager', () => { // the same in that case assert.strictEqual(source.toString(), result?.resource.toString()); } else { - if (resolveSource || resolveTarget) { - assert.strictEqual(target.toString(), result?.resource.toString()); - } else { - if (accessor.uriIdentityService.extUri.isEqual(source, target)) { - assert.strictEqual(undefined, result); - } else { - assert.strictEqual(target.toString(), result?.resource.toString()); - } - } + assert.strictEqual(target.toString(), result?.resource.toString()); } if (resolveSource) { diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 28119fb7798..8a7b56c90d3 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -8,8 +8,7 @@ import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; -import { IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; +import { FileWorkingCopyManager2, IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; @@ -20,8 +19,7 @@ suite('UntitledFileWorkingCopyManager', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let manager: IUntitledFileWorkingCopyManager; - let fileManager: IFileWorkingCopyManager; + let manager: IFileWorkingCopyManager2; setup(() => { instantiationService = workbenchInstantiationService(); @@ -30,38 +28,16 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); - fileManager = new FileWorkingCopyManager( - 'testFileWorkingCopyType', + manager = new FileWorkingCopyManager2( + 'testFileWorkingCopyTypeUntitled', new TestFileWorkingCopyModelFactory(), - accessor.fileService, - accessor.lifecycleService, - accessor.labelService, - instantiationService, - accessor.logService, - accessor.fileDialogService, - accessor.workingCopyFileService, - accessor.workingCopyBackupService, - accessor.uriIdentityService, - accessor.dialogService, - accessor.workingCopyService, - accessor.environmentService, - accessor.pathService - ); - - manager = new UntitledFileWorkingCopyManager( - 'testUntitledWorkingCopyType', new TestUntitledFileWorkingCopyModelFactory(), - resource => fileManager.resolve(resource), accessor.fileService, - accessor.labelService, instantiationService, - accessor.logService, accessor.fileDialogService, accessor.workingCopyFileService, - accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.dialogService, - accessor.workingCopyService, accessor.environmentService, accessor.pathService ); @@ -73,29 +49,29 @@ suite('UntitledFileWorkingCopyManager', () => { test('basics', async () => { let disposeCounter = 0; - manager.onWillDispose(e => { + manager.untitled.onWillDispose(e => { disposeCounter++; }); let dirtyCounter = 0; - manager.onDidChangeDirty(e => { + manager.untitled.onDidChangeDirty(e => { dirtyCounter++; }); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); - assert.strictEqual(manager.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); - assert.strictEqual(manager.get(URI.file('/some/invalidPath')), undefined); - assert.strictEqual(manager.get(URI.file('/some/invalidPath').with({ scheme: Schemas.untitled })), undefined); + assert.strictEqual(manager.untitled.get(URI.file('/some/invalidPath')), undefined); + assert.strictEqual(manager.untitled.get(URI.file('/some/invalidPath').with({ scheme: Schemas.untitled })), undefined); - const workingCopy1 = await manager.resolve(); - const workingCopy2 = await manager.resolve(); + const workingCopy1 = await manager.untitled.resolve(); + const workingCopy2 = await manager.untitled.resolve(); - assert.strictEqual(manager.get(workingCopy1.resource), workingCopy1); - assert.strictEqual(manager.get(workingCopy2.resource), workingCopy2); + assert.strictEqual(manager.untitled.get(workingCopy1.resource), workingCopy1); + assert.strictEqual(manager.untitled.get(workingCopy2.resource), workingCopy2); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); - assert.strictEqual(manager.workingCopies.length, 2); + assert.strictEqual(manager.untitled.workingCopies.length, 2); assert.notStrictEqual(workingCopy1.resource.toString(), workingCopy2.resource.toString()); @@ -120,24 +96,24 @@ suite('UntitledFileWorkingCopyManager', () => { workingCopy1.dispose(); - assert.strictEqual(manager.workingCopies.length, 1); - assert.strictEqual(manager.get(workingCopy1.resource), undefined); + assert.strictEqual(manager.untitled.workingCopies.length, 1); + assert.strictEqual(manager.untitled.get(workingCopy1.resource), undefined); workingCopy2.dispose(); - assert.strictEqual(manager.workingCopies.length, 0); - assert.strictEqual(manager.get(workingCopy2.resource), undefined); + assert.strictEqual(manager.untitled.workingCopies.length, 0); + assert.strictEqual(manager.untitled.get(workingCopy2.resource), undefined); assert.strictEqual(disposeCounter, 2); }); test('resolve - with initial value', async () => { let dirtyCounter = 0; - manager.onDidChangeDirty(e => { + manager.untitled.onDidChangeDirty(e => { dirtyCounter++; }); - const workingCopy = await manager.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello World')) }); + const workingCopy = await manager.untitled.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello World')) }); assert.strictEqual(workingCopy.isDirty(), true); assert.strictEqual(dirtyCounter, 1); @@ -147,12 +123,12 @@ suite('UntitledFileWorkingCopyManager', () => { }); test('resolve - existing', async () => { - const workingCopy1 = await manager.resolve(); + const workingCopy1 = await manager.untitled.resolve(); - const workingCopy2 = await manager.resolve({ untitledResource: workingCopy1.resource }); + const workingCopy2 = await manager.untitled.resolve({ untitledResource: workingCopy1.resource }); assert.strictEqual(workingCopy1, workingCopy2); - const workingCopy3 = await manager.resolve({ untitledResource: URI.file('/invalid/untitled') }); + const workingCopy3 = await manager.untitled.resolve({ untitledResource: URI.file('/invalid/untitled') }); assert.strictEqual(workingCopy3.resource.scheme, Schemas.untitled); workingCopy1.dispose(); @@ -161,7 +137,7 @@ suite('UntitledFileWorkingCopyManager', () => { }); test('resolve - with associated resource', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); assert.strictEqual(workingCopy.hasAssociatedFilePath, true); assert.strictEqual(workingCopy.resource.path, '/some/associated.txt'); @@ -170,7 +146,7 @@ suite('UntitledFileWorkingCopyManager', () => { }); test('save - without associated resource', async () => { - const workingCopy = await manager.resolve(); + const workingCopy = await manager.untitled.resolve(); workingCopy.model?.updateContents('Simple Save'); accessor.fileDialogService.setPickFileToSave(URI.file('simple/file.txt')); @@ -178,13 +154,13 @@ suite('UntitledFileWorkingCopyManager', () => { const result = await workingCopy.save(); assert.ok(result); - assert.strictEqual(manager.get(workingCopy.resource), undefined); + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); workingCopy.dispose(); }); test('save - with associated resource', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save with associated resource'); accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }), true); @@ -192,79 +168,26 @@ suite('UntitledFileWorkingCopyManager', () => { const result = await workingCopy.save(); assert.ok(result); - assert.strictEqual(manager.get(workingCopy.resource), undefined); + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); workingCopy.dispose(); }); test('save - with associated resource (asks to overwrite)', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save with associated resource'); let result = await workingCopy.save(); assert.ok(!result); // not confirmed - assert.strictEqual(manager.get(workingCopy.resource), workingCopy); + assert.strictEqual(manager.untitled.get(workingCopy.resource), workingCopy); accessor.dialogService.setConfirmResult({ confirmed: true }); result = await workingCopy.save(); assert.ok(result); // confirmed - assert.strictEqual(manager.get(workingCopy.resource), undefined); - - workingCopy.dispose(); - }); - - test('saveAs - without associated resource', async () => { - const workingCopy = await manager.resolve(); - workingCopy.model?.updateContents('Simple Save As'); - - const target = URI.file('simple/file.txt'); - accessor.fileDialogService.setPickFileToSave(target); - - const result = await manager.saveAs(workingCopy.resource, undefined); - assert.strictEqual(result?.resource.toString(), target.toString()); - - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); - - assert.strictEqual(manager.get(workingCopy.resource), undefined); - - workingCopy.dispose(); - }); - - test('saveAs - with associated resource', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); - workingCopy.model?.updateContents('Simple Save As with associated resource'); - - const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); - - accessor.fileService.notExistsSet.set(target, true); - - const result = await manager.saveAs(workingCopy.resource, undefined); - assert.strictEqual(result?.resource.toString(), target.toString()); - - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); - - assert.strictEqual(manager.get(workingCopy.resource), undefined); - - workingCopy.dispose(); - }); - - test('saveAs - target exists and is resolved', async () => { - const workingCopy = await manager.resolve(); - workingCopy.model?.updateContents('Simple Save As'); - - const target = URI.file('simple/file.txt'); - const targetFileWorkingCopy = await fileManager.resolve(target); - accessor.fileDialogService.setPickFileToSave(target); - - const result = await manager.saveAs(workingCopy.resource, undefined); - assert.strictEqual(result, targetFileWorkingCopy); - - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); - - assert.strictEqual(manager.get(workingCopy.resource), undefined); + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); workingCopy.dispose(); }); @@ -272,16 +195,16 @@ suite('UntitledFileWorkingCopyManager', () => { test('destroy', async () => { assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); - await manager.resolve(); - await manager.resolve(); - await manager.resolve(); + await manager.untitled.resolve(); + await manager.untitled.resolve(); + await manager.untitled.resolve(); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); - assert.strictEqual(manager.workingCopies.length, 3); + assert.strictEqual(manager.untitled.workingCopies.length, 3); - await manager.destroy(); + await manager.untitled.destroy(); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); - assert.strictEqual(manager.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); }); }); From f22eb7ee29dec4dbe5936438a3dd2847f9d16789 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 09:17:03 +0200 Subject: [PATCH 073/270] untitled file working copy - tests for new unified manager --- .../browser/fileWorkingCopyManager2.test.ts | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts new file mode 100644 index 00000000000..dfa89d1b333 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { workbenchInstantiationService, TestServiceAccessor, TestInMemoryFileSystemProvider } from 'vs/workbench/test/browser/workbenchTestServices'; +import { FileWorkingCopy, IFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; +import { Schemas } from 'vs/base/common/network'; +import { IFileWorkingCopyManager2, FileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; +import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; +import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; + +suite('FileWorkingCopyManager2', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + let manager: IFileWorkingCopyManager2; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); + accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); + + manager = new FileWorkingCopyManager2( + 'testFileWorkingCopyType2', + new TestFileWorkingCopyModelFactory(), + new TestUntitledFileWorkingCopyModelFactory(), + accessor.fileService, + instantiationService, + accessor.fileDialogService, + accessor.workingCopyFileService, + accessor.uriIdentityService, + accessor.dialogService, + accessor.environmentService, + accessor.pathService + ); + }); + + teardown(() => { + manager.dispose(); + }); + + test('resolve', async () => { + const fileWorkingCopy = await manager.resolve(URI.file('/test.html')); + assert.ok(fileWorkingCopy instanceof FileWorkingCopy); + assert.strictEqual(await manager.files.resolve(fileWorkingCopy.resource), fileWorkingCopy); + + const untitledFileWorkingCopy = await manager.resolve(); + assert.ok(untitledFileWorkingCopy instanceof UntitledFileWorkingCopy); + assert.strictEqual(await manager.untitled.resolve({ untitledResource: untitledFileWorkingCopy.resource }), untitledFileWorkingCopy); + + fileWorkingCopy.dispose(); + untitledFileWorkingCopy.dispose(); + }); + + test('destroy', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + + await manager.resolve(URI.file('/test.html')); + await manager.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello Untitled')) }); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); + assert.strictEqual(manager.files.workingCopies.length, 1); + assert.strictEqual(manager.untitled.workingCopies.length, 1); + + await manager.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.files.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); + }); + + // saveAs: unresolved source, unresolved target + + test('saveAs - file (same target, unresolved source, unresolved target)', () => { + const source = URI.file('/path/source.txt'); + + return testSaveAsFile(source, source, false, false); + }); + + test('saveAs - file (same target, different case, unresolved source, unresolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/SOURCE.txt'); + + return testSaveAsFile(source, target, false, false); + }); + + test('saveAs - file (different target, unresolved source, unresolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/target.txt'); + + return testSaveAsFile(source, target, false, false); + }); + + // saveAs: resolved source, unresolved target + + test('saveAs - file (same target, resolved source, unresolved target)', () => { + const source = URI.file('/path/source.txt'); + + return testSaveAsFile(source, source, true, false); + }); + + test('saveAs - file (same target, different case, resolved source, unresolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/SOURCE.txt'); + + return testSaveAsFile(source, target, true, false); + }); + + test('saveAs - file (different target, resolved source, unresolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/target.txt'); + + return testSaveAsFile(source, target, true, false); + }); + + // saveAs: unresolved source, resolved target + + test('saveAs - file (same target, unresolved source, resolved target)', () => { + const source = URI.file('/path/source.txt'); + + return testSaveAsFile(source, source, false, true); + }); + + test('saveAs - file (same target, different case, unresolved source, resolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/SOURCE.txt'); + + return testSaveAsFile(source, target, false, true); + }); + + test('saveAs - file (different target, unresolved source, resolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/target.txt'); + + return testSaveAsFile(source, target, false, true); + }); + + // saveAs: resolved source, resolved target + + test('saveAs - file (same target, resolved source, resolved target)', () => { + const source = URI.file('/path/source.txt'); + + return testSaveAsFile(source, source, true, true); + }); + + test('saveAs - file (different target, resolved source, resolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/target.txt'); + + return testSaveAsFile(source, target, true, true); + }); + + async function testSaveAsFile(source: URI, target: URI, resolveSource: boolean, resolveTarget: boolean) { + let sourceWorkingCopy: IFileWorkingCopy | undefined = undefined; + if (resolveSource) { + sourceWorkingCopy = await manager.resolve(source); + sourceWorkingCopy.model?.updateContents('hello world'); + assert.ok(sourceWorkingCopy.isDirty()); + } + + let targetWorkingCopy: IFileWorkingCopy | undefined = undefined; + if (resolveTarget) { + targetWorkingCopy = await manager.resolve(target); + targetWorkingCopy.model?.updateContents('hello world'); + assert.ok(targetWorkingCopy.isDirty()); + } + + const result = await manager.saveAs(source, target); + if (accessor.uriIdentityService.extUri.isEqual(source, target) && resolveSource) { + // if the uris are considered equal (different case on macOS/Windows) + // and the source is to be resolved, the resulting working copy resource + // will be the source resource because we consider file working copies + // the same in that case + assert.strictEqual(source.toString(), result?.resource.toString()); + } else { + if (resolveSource || resolveTarget) { + assert.strictEqual(target.toString(), result?.resource.toString()); + } else { + if (accessor.uriIdentityService.extUri.isEqual(source, target)) { + assert.strictEqual(undefined, result); + } else { + assert.strictEqual(target.toString(), result?.resource.toString()); + } + } + } + + if (resolveSource) { + assert.strictEqual(sourceWorkingCopy?.isDirty(), false); + } + + if (resolveTarget) { + assert.strictEqual(targetWorkingCopy?.isDirty(), false); + } + } + + test('saveAs - untitled (without associated resource)', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - untitled (with associated resource)', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save As with associated resource'); + + const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); + + accessor.fileService.notExistsSet.set(target, true); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - untitled (target exists and is resolved)', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + const targetFileWorkingCopy = await manager.resolve(target); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result, targetFileWorkingCopy); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); +}); From a48180b6e6dfd70de2321f178dd3746184d2d591 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 19 May 2021 09:17:29 +0200 Subject: [PATCH 074/270] valide URIs created via from, https://github.com/microsoft/vscode/issues/121198 --- src/vs/base/common/uri.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index c2b0b02eb86..da8c4de410d 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -327,13 +327,15 @@ export class URI implements UriComponents { } static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { - return new Uri( + const result = new Uri( components.scheme, components.authority, components.path, components.query, components.fragment, ); + _validateUri(result, true); + return result; } /** From 13aff6aa7d3b2413ee0081712a30d797ae3aea57 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 09:37:10 +0200 Subject: [PATCH 075/270] untitled file working copy - test :lipstick: --- .../test/browser/fileWorkingCopy.test.ts | 19 +++++++++++++++-- .../browser/fileWorkingCopyManager.test.ts | 21 +++++++++++++++++-- .../browser/fileWorkingCopyManager2.test.ts | 8 ------- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index e01b543faa1..cfbfbdfc664 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -495,8 +495,6 @@ suite('FileWorkingCopy', function () { accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); await workingCopy.save({ force: true }); - } catch (error) { - // error is expected } finally { accessor.fileService.writeShouldThrowError = undefined; } @@ -559,6 +557,23 @@ suite('FileWorkingCopy', function () { assert.strictEqual(workingCopy.isDirty(), false); }); + test('save (errors, bubbles up with `ignoreErrorHandler`)', async () => { + await workingCopy.resolve(); + + let error: Error | undefined = undefined; + try { + accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); + + await workingCopy.save({ force: true, ignoreErrorHandler: true }); + } catch (e) { + error = e; + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + + assert.ok(error); + }); + test('revert', async () => { await workingCopy.resolve(); workingCopy.model?.updateContents('hello revert'); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 9905e3ac630..a0a4199ff0b 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -10,7 +10,7 @@ import { workbenchInstantiationService, TestServiceAccessor, TestWillShutdownEve import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; -import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; +import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { timeout } from 'vs/base/common/async'; import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -189,6 +189,7 @@ suite('FileWorkingCopyManager', () => { let gotNonDirtyCounter = 0; let revertedCounter = 0; let savedCounter = 0; + let saveErrorCounter = 0; manager.onDidCreate(workingCopy => { createdCounter++; @@ -222,6 +223,12 @@ suite('FileWorkingCopyManager', () => { } }); + manager.onDidSaveError(workingCopy => { + if (workingCopy.resource.toString() === resource1.toString()) { + saveErrorCounter++; + } + }); + const workingCopy1 = await manager.resolve(resource1); assert.strictEqual(resolvedCounter, 1); assert.strictEqual(createdCounter, 1); @@ -239,14 +246,24 @@ suite('FileWorkingCopyManager', () => { workingCopy1.model?.updateContents('changed again'); await workingCopy1.save(); + + try { + accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); + + await workingCopy1.save({ force: true }); + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + workingCopy1.dispose(); workingCopy2.dispose(); await workingCopy1.revert(); - assert.strictEqual(gotDirtyCounter, 2); + assert.strictEqual(gotDirtyCounter, 3); assert.strictEqual(gotNonDirtyCounter, 2); assert.strictEqual(revertedCounter, 1); assert.strictEqual(savedCounter, 1); + assert.strictEqual(saveErrorCounter, 1); assert.strictEqual(createdCounter, 2); workingCopy1.dispose(); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts index dfa89d1b333..ae6a5510c04 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts @@ -78,8 +78,6 @@ suite('FileWorkingCopyManager2', () => { assert.strictEqual(manager.untitled.workingCopies.length, 0); }); - // saveAs: unresolved source, unresolved target - test('saveAs - file (same target, unresolved source, unresolved target)', () => { const source = URI.file('/path/source.txt'); @@ -100,8 +98,6 @@ suite('FileWorkingCopyManager2', () => { return testSaveAsFile(source, target, false, false); }); - // saveAs: resolved source, unresolved target - test('saveAs - file (same target, resolved source, unresolved target)', () => { const source = URI.file('/path/source.txt'); @@ -122,8 +118,6 @@ suite('FileWorkingCopyManager2', () => { return testSaveAsFile(source, target, true, false); }); - // saveAs: unresolved source, resolved target - test('saveAs - file (same target, unresolved source, resolved target)', () => { const source = URI.file('/path/source.txt'); @@ -144,8 +138,6 @@ suite('FileWorkingCopyManager2', () => { return testSaveAsFile(source, target, false, true); }); - // saveAs: resolved source, resolved target - test('saveAs - file (same target, resolved source, resolved target)', () => { const source = URI.file('/path/source.txt'); From 6034e35043e806d8961e9f730d8c78b244cb3c37 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 18 May 2021 14:38:56 +0200 Subject: [PATCH 076/270] Additional details in the aria label --- .../contrib/workspace/browser/workspace.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 148c23121e1..a41c0bdbd69 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -202,6 +202,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben return { id: BANNER_RESTRICTED_MODE, icon: shieldIcon, + ariaLabel: localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use left, right navigation keys to access banner actions."), message: localize('restrictedModeBannerMessage', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."), actions: [ { From 501b691dd0648671ec22e104fa4a3366aacec445 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 19 May 2021 09:51:15 +0200 Subject: [PATCH 077/270] Additional navigation keys --- src/vs/workbench/browser/parts/banner/bannerPart.ts | 2 ++ .../contrib/workspace/browser/workspace.contribution.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 1665bb78406..4a042ca9d19 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -288,6 +288,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.banner.focusNextAction', weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.RightArrow, + secondary: [KeyCode.DownArrow], when: CONTEXT_BANNER_FOCUSED, handler: (accessor: ServicesAccessor) => { const bannerService = accessor.get(IBannerService); @@ -299,6 +300,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.banner.focusPreviousAction', weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.LeftArrow, + secondary: [KeyCode.UpArrow], when: CONTEXT_BANNER_FOCUSED, handler: (accessor: ServicesAccessor) => { const bannerService = accessor.get(IBannerService); diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index a41c0bdbd69..902e74d93d5 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -202,7 +202,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben return { id: BANNER_RESTRICTED_MODE, icon: shieldIcon, - ariaLabel: localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use left, right navigation keys to access banner actions."), + ariaLabel: localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use navigation keys to access banner actions."), message: localize('restrictedModeBannerMessage', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."), actions: [ { From c5611075cadbff5f545941df15def7074f3c5426 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 19 May 2021 10:03:43 +0200 Subject: [PATCH 078/270] fix ExtensionRecommendationsService test (for #124119) --- .../extensionRecommendationsService.ts | 10 +++++-- .../extensionRecommendationsService.test.ts | 30 +++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 56aff6ead14..b353d267df2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -28,8 +28,6 @@ type IgnoreRecommendationClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; }; -const WORKSPACE_RECOMMENDATION_DELAY = 5000; - export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService { declare readonly _serviceBrand: undefined; @@ -235,13 +233,19 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return !this.extensionRecommendationsManagementService.ignoredRecommendations.includes(extensionId.toLowerCase()); } + // for testing + protected get workbenchRecommendationDelay() { + // remote extensions might still being installed #124119 + return 5000; + } + private async promptWorkspaceRecommendations(): Promise { const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations] .map(({ extensionId }) => extensionId) .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); if (allowedRecommendations.length) { - await timeout(WORKSPACE_RECOMMENDATION_DELAY); // remote extensions might still being installed #124119 + await timeout(this.workbenchRecommendationDelay); await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations); } } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index efedc1c28dd..9798f435ebf 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -175,6 +175,12 @@ function aGalleryExtension(name: string, properties: any = {}, galleryExtensionP return galleryExtension; } +class TestExtensionRecommendationsService extends ExtensionRecommendationsService { + protected override get workbenchRecommendationDelay() { + return 0; + } +} + suite('ExtensionRecommendationsService Test', () => { let workspaceService: IWorkspaceContextService; let instantiationService: TestInstantiationService; @@ -311,7 +317,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.strictEqual(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); @@ -321,7 +327,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); assert.ok(!prompted); return testObject.getWorkspaceRecommendations().then(() => { @@ -350,7 +356,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => { await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await Event.toPromise(promptedEmitter.event); const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); @@ -379,7 +385,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if ignoreRecommendations is set', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -389,7 +395,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if showRecommendationsOnlyOnDemand is set', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -407,7 +413,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored @@ -425,7 +431,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored @@ -447,7 +453,7 @@ suite('ExtensionRecommendationsService Test', () => { storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await testObject.activationPromise; const recommendations = testObject.getAllRecommendationsWithReason(); @@ -467,7 +473,7 @@ suite('ExtensionRecommendationsService Test', () => { await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await testObject.activationPromise; let recommendations = testObject.getAllRecommendationsWithReason(); @@ -499,7 +505,7 @@ suite('ExtensionRecommendationsService Test', () => { storageService.store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.GLOBAL, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', []); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); extensionIgnoredRecommendationsService.onDidChangeGlobalIgnoredRecommendation(changeHandlerTarget); extensionIgnoredRecommendationsService.toggleGlobalIgnoredRecommendation(ignoredExtensionId, true); @@ -514,7 +520,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); @@ -533,7 +539,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); From a9c5e37ea0dc020f5df3b1c82ef020627fc8cd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 19 May 2021 10:38:47 +0200 Subject: [PATCH 079/270] fix #124102 --- src/vs/workbench/contrib/update/browser/update.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index e294db7c023..473dbf7f0d9 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -275,10 +275,18 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu if (state.type === StateType.AvailableForDownload || state.type === StateType.Downloaded || state.type === StateType.Ready) { badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameShort)); - } else if (state.type === StateType.CheckingForUpdates || state.type === StateType.Downloading || state.type === StateType.Updating) { + } else if (state.type === StateType.CheckingForUpdates) { badge = new ProgressBadge(() => nls.localize('checkingForUpdates', "Checking for Updates...")); clazz = 'progress-badge'; priority = 1; + } else if (state.type === StateType.Downloading) { + badge = new ProgressBadge(() => nls.localize('downloading', "Downloading...")); + clazz = 'progress-badge'; + priority = 1; + } else if (state.type === StateType.Updating) { + badge = new ProgressBadge(() => nls.localize('updating', "Updating...")); + clazz = 'progress-badge'; + priority = 1; } this.badgeDisposable.clear(); From df308a9a81b4acd6ac8b40fdfde97cb3261820f7 Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Wed, 5 May 2021 23:21:36 -0500 Subject: [PATCH 080/270] add color customizations for inline debug values --- .../debug/browser/debugEditorContribution.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index edec9fd5d18..02c88afc9e4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -45,6 +45,8 @@ import { Event } from 'vs/base/common/event'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Expression } from 'vs/workbench/contrib/debug/common/debugModel'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; const LAUNCH_JSON_REGEX = /\.vscode\/launch\.json$/; const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration'; @@ -52,6 +54,20 @@ const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped +export const debugInlineForeground = registerColor('debugInline.decorationForeground', { + dark: '#ffffff80', + light: '#00000080', + hc: '#ffffff80' +}, nls.localize('debug.inline.decorationForeground', "Color for the inline debug text.")); + +export const debugInlineBackground = registerColor('debugInline.decorationBackground', { + dark: '#ffc80033', + light: '#ffc80033', + hc: '#ffc80033' +}, nls.localize('debug.inline.decorationBackground', "Color for the inline debug background.")); + + + class InlineSegment { constructor(public column: number, public text: string) { } @@ -73,18 +89,9 @@ function createInlineValueDecoration(lineNumber: number, contentText: string, co renderOptions: { after: { contentText, - backgroundColor: 'rgba(255, 200, 0, 0.2)', - margin: '10px' - }, - dark: { - after: { - color: 'rgba(255, 255, 255, 0.5)', - } - }, - light: { - after: { - color: 'rgba(0, 0, 0, 0.5)', - } + backgroundColor: themeColorFromId(debugInlineBackground), + margin: '10px', + color: themeColorFromId(debugInlineForeground) } } }; From 0e4159cb7aab4f5585a23c587be884f3cd958628 Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Wed, 5 May 2021 23:33:40 -0500 Subject: [PATCH 081/270] fix localization key/description for debugInline colors --- .../contrib/debug/browser/debugEditorContribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 02c88afc9e4..efab2dfda5f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -58,13 +58,13 @@ export const debugInlineForeground = registerColor('debugInline.decorationForegr dark: '#ffffff80', light: '#00000080', hc: '#ffffff80' -}, nls.localize('debug.inline.decorationForeground', "Color for the inline debug text.")); +}, nls.localize('debugInline.decorationForeground', "Color for the debug inline value text.")); export const debugInlineBackground = registerColor('debugInline.decorationBackground', { dark: '#ffc80033', light: '#ffc80033', hc: '#ffc80033' -}, nls.localize('debug.inline.decorationBackground', "Color for the inline debug background.")); +}, nls.localize('debugInline.decorationBackground', "Color for the debug inline value background.")); From 677f2a3be1267173d3f148898b49da8f608fe2d4 Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Thu, 6 May 2021 22:35:59 -0500 Subject: [PATCH 082/270] remove excess whitespace --- .../workbench/contrib/debug/browser/debugEditorContribution.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index efab2dfda5f..6c9807f41bb 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -66,8 +66,6 @@ export const debugInlineBackground = registerColor('debugInline.decorationBackgr hc: '#ffc80033' }, nls.localize('debugInline.decorationBackground', "Color for the debug inline value background.")); - - class InlineSegment { constructor(public column: number, public text: string) { } From b1349b64a64addf2829d84f076ba63346742a6f6 Mon Sep 17 00:00:00 2001 From: Nick Rayburn Date: Tue, 18 May 2021 15:56:37 -0500 Subject: [PATCH 083/270] update color registry names for inline values --- .../contrib/debug/browser/debugEditorContribution.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 6c9807f41bb..979f3ef9d94 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -54,17 +54,17 @@ const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped -export const debugInlineForeground = registerColor('debugInline.decorationForeground', { +export const debugInlineForeground = registerColor('editor.inlineValuesForeground', { dark: '#ffffff80', light: '#00000080', hc: '#ffffff80' -}, nls.localize('debugInline.decorationForeground', "Color for the debug inline value text.")); +}, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text.")); -export const debugInlineBackground = registerColor('debugInline.decorationBackground', { +export const debugInlineBackground = registerColor('editor.inlineValuesBackground', { dark: '#ffc80033', light: '#ffc80033', hc: '#ffc80033' -}, nls.localize('debugInline.decorationBackground', "Color for the debug inline value background.")); +}, nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); class InlineSegment { constructor(public column: number, public text: string) { From 825f6c7ab84d281acb5416031474a0916054c916 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 19 May 2021 09:17:29 +0200 Subject: [PATCH 084/270] valide URIs created via from, https://github.com/microsoft/vscode/issues/121198 --- src/vs/base/common/uri.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index c2b0b02eb86..da8c4de410d 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -327,13 +327,15 @@ export class URI implements UriComponents { } static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { - return new Uri( + const result = new Uri( components.scheme, components.authority, components.path, components.query, components.fragment, ); + _validateUri(result, true); + return result; } /** From 4173ced659cbf169f5e48b719ccc4fef7296f580 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 17 May 2021 11:44:00 +0200 Subject: [PATCH 085/270] untitled file working copy - first cut --- .../common/untitledTextEditorModel.ts | 75 ++--- .../test/browser/untitledTextEditor.test.ts | 16 - .../workingCopy/common/fileWorkingCopy.ts | 10 +- .../common/untitledFileWorkingCopy.ts | 281 ++++++++++++++++++ 4 files changed, 319 insertions(+), 63 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 732985245f5..7f4557bdde7 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ITextModel } from 'vs/editor/common/model'; -import { createTextBufferFactory, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; +import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -63,12 +63,6 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport * Resolves the untitled model. */ resolve(): Promise; - - /** - * Updates the value of the untitled model optionally allowing to ignore dirty. - * The model must be resolved for this method to work. - */ - setValue(value: string, ignoreDirty?: boolean): void; } export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel { @@ -99,6 +93,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt readonly capabilities = WorkingCopyCapabilities.Untitled; + //#region Name + + private configuredLabelFormat: 'content' | 'name' = 'content'; + private cachedModelFirstLineWords: string | undefined = undefined; get name(): string { // Take name from first line if present and only if @@ -112,13 +110,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.labelService.getUriBasenameLabel(this.resource); } - private dirty = this.hasAssociatedFilePath || !!this.initialValue; - private ignoreDirtyOnModelContentChange = false; + //#endregion - private versionId = 0; - - private configuredEncoding: string | undefined; - private configuredLabelFormat: 'content' | 'name' = 'content'; constructor( public readonly resource: URI, @@ -153,7 +146,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private registerListeners(): void { // Config Changes - this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.onConfigurationChange(true))); + this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => this.onConfigurationChange(true))); } private onConfigurationChange(fromEvent: boolean): void { @@ -179,9 +172,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } } - getVersionId(): number { - return this.versionId; - } + + //#region Mode private _hasModeSetExplicitly: boolean = false; get hasModeSetExplicitly(): boolean { return this._hasModeSetExplicitly; } @@ -216,6 +208,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.preferredMode; } + //#endregion + + + //#region Encoding + + private configuredEncoding: string | undefined; + getEncoding(): string | undefined { return this.preferredEncoding || this.configuredEncoding; } @@ -230,25 +229,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } } - setValue(value: string, ignoreDirty?: boolean): void { - if (ignoreDirty) { - this.ignoreDirtyOnModelContentChange = true; - } - - try { - this.updateTextEditorModel(createTextBufferFactory(value)); - } finally { - this.ignoreDirtyOnModelContentChange = false; - } - } - - override isReadonly(): boolean { - return false; - } + //#endregion //#region Dirty + private dirty = this.hasAssociatedFilePath || !!this.initialValue; + isDirty(): boolean { return this.dirty; } @@ -360,19 +347,16 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } private onModelContentChanged(textEditorModel: ITextModel, e: IModelContentChangedEvent): void { - this.versionId++; - if (!this.ignoreDirtyOnModelContentChange) { - // mark the untitled text editor as non-dirty once its content becomes empty and we do - // not have an associated path set. we never want dirty indicator in that case. - if (!this.hasAssociatedFilePath && textEditorModel.getLineCount() === 1 && textEditorModel.getLineContent(1) === '') { - this.setDirty(false); - } + // mark the untitled text editor as non-dirty once its content becomes empty and we do + // not have an associated path set. we never want dirty indicator in that case. + if (!this.hasAssociatedFilePath && textEditorModel.getLineCount() === 1 && textEditorModel.getLineContent(1) === '') { + this.setDirty(false); + } - // turn dirty otherwise - else { - this.setDirty(true); - } + // turn dirty otherwise + else { + this.setDirty(true); } // Check for name change if first line changed in the range of 0-FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH columns @@ -421,4 +405,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } //#endregion + + + override isReadonly(): boolean { + return false; + } } diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index e3abf1938ea..2eea7d8a629 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -102,22 +102,6 @@ suite('Untitled text editors', () => { }); } - test('setValue()', async () => { - const service = accessor.untitledTextEditorService; - const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - - const model = await untitled.resolve(); - - model.setValue('not dirty', true); - assert.ok(!model.isDirty()); - - model.setValue('dirty'); - assert.ok(model.isDirty()); - - untitled.dispose(); - model.dispose(); - }); - test('associated resource is dirty', async () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index 5d573428ee8..ba40ccd1df1 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -445,6 +445,10 @@ export class FileWorkingCopy extends ResourceWo private lastResolvedFileStat: IFileStatWithMetadata | undefined; + isResolved(): this is IResolvedFileWorkingCopy { + return !!this.model; + } + async resolve(options?: IFileWorkingCopyResolveOptions): Promise { this.trace('[file working copy] resolve() - enter'); @@ -1172,6 +1176,8 @@ export class FileWorkingCopy extends ResourceWo return; // ignore if not resolved or not dirty and not enforced } + this.trace('[file working copy] revert()'); + // Unset flags const wasDirty = this.dirty; const undoSetDirty = this.doSetDirty(false); @@ -1235,10 +1241,6 @@ export class FileWorkingCopy extends ResourceWo //#region Utilities - isResolved(): this is IResolvedFileWorkingCopy { - return !!this.model; - } - isReadonly(): boolean { return this.lastResolvedFileStat?.readonly || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts new file mode 100644 index 00000000000..02ed75d2d8e --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -0,0 +1,281 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ISaveOptions } from 'vs/workbench/common/editor'; +import { raceCancellation } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { emptyStream } from 'vs/base/common/stream'; + +export interface IUntitledFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } + +export interface IUntitledFileWorkingCopyModel extends IFileWorkingCopyModel { + readonly onDidChangeContent: Event; +} + +export interface IUntitledFileWorkingCopyModelContentChangedEvent extends IFileWorkingCopyModelContentChangedEvent { + + /** + * Flag that indicates that the content change + * resulted in empty contents. A untitled file + * working copy without contents may be marked + * as non-dirty. + */ + readonly isEmpty: boolean; +} + +export interface IUntitledFileWorkingCopy extends IWorkingCopy { + + /** + * Emits an event when this untitled model is reverted. + */ + readonly onDidRevert: Event; + + /** + * Provides access to the underlying model of this untitled + * file based working copy. As long as the untitled file working + * copy has not been resolved, the model is `undefined`. + */ + readonly model: T | undefined; + + /** + * Whether this untitled file working copy model has an associated file path. + */ + readonly hasAssociatedFilePath: boolean; + + /** + * Resolves an untitled file working copy. + */ + resolve(): Promise; +} + +export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { + + /** + * A resolved untitled file working copy has a resolved model `T`. + */ + readonly model: T; +} + +export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { + + readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.Untitled; + + private _model: T | undefined = undefined; + get model(): T | undefined { return this._model; } + + //#region Events + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidRevert = this._register(new Emitter()); + readonly onDidRevert = this._onDidRevert.event; + + //#endregion + + constructor( + readonly typeId: string, + readonly resource: URI, + readonly name: string, + readonly hasAssociatedFilePath: boolean, + private readonly initialValue: VSBufferReadableStream | undefined, + private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @ILogService private readonly logService: ILogService + ) { + super(); + + if (resource.scheme !== Schemas.untitled) { + throw new Error(`The untitled file working copy resource ${this.resource.toString(true)} is not using untitled as scheme.`); + } + + // Make known to working copy service + this._register(workingCopyService.registerWorkingCopy(this)); + } + + //#region Dirty + + private dirty = this.hasAssociatedFilePath || !!this.initialValue; + + isDirty(): boolean { + return this.dirty; + } + + private setDirty(dirty: boolean): void { + if (this.dirty === dirty) { + return; + } + + this.dirty = dirty; + this._onDidChangeDirty.fire(); + } + + //#endregion + + + //#region Resolve + + async resolve(): Promise { + this.trace('[untitled file working copy] resolve()'); + + if (this.isResolved()) { + this.trace('[untitled file working copy] resolve() - exit (already resolved)'); + + // return early if the untitled file working copy is already + // resolved assuming that the contents have meanwhile changed + // in the underlying model. we only resolve untitled once. + return; + } + + let untitledContents: VSBufferReadableStream; + + // Check for backups or use initial value or empty + const backup = await this.workingCopyBackupService.resolve(this); + if (backup) { + this.trace('[untitled file working copy] resolve() - with backup'); + + untitledContents = backup.value; + } else if (this.initialValue) { + this.trace('[untitled file working copy] resolve() - with initial contents'); + + untitledContents = this.initialValue; + } else { + this.trace('[untitled file working copy] resolve() - empty'); + + untitledContents = emptyStream(); + } + + // Create model + await this.doCreateModel(untitledContents); + + // Untitled associated to file path are dirty right away as well as untitled with content + this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue); + + // If we have initial contents, make sure to emit this + // as the appropiate events to the outside. + if (!!backup || this.initialValue) { + this._onDidChangeContent.fire(); + } + } + + private async doCreateModel(contents: VSBufferReadableStream): Promise { + this.trace('[untitled file working copy] doCreateModel()'); + + // Create model and dispose it when we get disposed + this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); + + // Model listeners + this.installModelListeners(this._model); + } + + private installModelListeners(model: IUntitledFileWorkingCopyModel): void { + + // Content Change + this._register(model.onDidChangeContent(e => this.onModelContentChanged(e))); + + // Lifecycle + this._register(model.onWillDispose(() => this.dispose())); + } + + private onModelContentChanged(e: IUntitledFileWorkingCopyModelContentChangedEvent): void { + + // Mark the untitled file working copy as non-dirty once its + // content becomes empty and we do not have an associated + // path set. we never want dirty indicator in that case. + if (!this.hasAssociatedFilePath && e.isEmpty) { + this.setDirty(false); + } + + // Turn dirty otherwise + else { + this.setDirty(true); + } + + // Emit as general content change event + this._onDidChangeContent.fire(); + } + + isResolved(): this is IResolvedUntitledFileWorkingCopy { + return !!this.model; + } + + //#endregion + + + //#region Backup + + async backup(token: CancellationToken): Promise { + + // Fill in content if we are resolved + let content: VSBufferReadableStream | undefined = undefined; + if (this.isResolved()) { + content = await raceCancellation(this.model.snapshot(token), token); + } + + return { content }; + } + + //#endregion + + + //#region Save + + save(options?: ISaveOptions): Promise { + this.trace('[untitled file working copy] save() - enter'); + + // TODO needs to extract the code for bringing up save dialog + // or use the associated file path as target + // Also, who is disposing the untitled after save and open the + // new editor? + + throw new Error('Method not implemented.'); + } + + //#endregion + + + //#region Revert + + async revert(): Promise { + this.trace('[untitled file working copy] revert()'); + + // No longer dirty + this.setDirty(false); + + // Emit as event + this._onDidRevert.fire(); + + // A reverted untitled file working copy is invalid + // because it has no actual source on disk to revert to. + // As such we dispose the model. + this.dispose(); + } + + //#endregion + + override dispose(): void { + this.trace('[untitled file working copy] dispose()'); + + super.dispose(); + } + + private trace(msg: string): void { + this.logService.trace(msg, this.resource.toString(true), this.typeId); + } +} From 9f990bbb6ecdb0d9fbcabb3d8a160d708d2c66f9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 17 May 2021 13:59:48 +0200 Subject: [PATCH 086/270] untitled file working copy - first cut manager --- .../common/fileWorkingCopyManager.ts | 11 +- .../common/untitledFileWorkingCopy.ts | 14 +- .../common/untitledFileWorkingCopyManager.ts | 308 ++++++++++++++++++ 3 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index a3ba3b249dc..72a7e3f3c5e 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -472,10 +472,17 @@ export class FileWorkingCopyManager extends Dis else { didCreateWorkingCopy = true; - const newWorkingCopy = workingCopy = this.instantiationService.createInstance(FileWorkingCopy, this.workingCopyTypeId, resource, this.labelService.getUriBasenameLabel(resource), this.modelFactory) as unknown as IFileWorkingCopy; + workingCopy = this.instantiationService.createInstance( + FileWorkingCopy, + this.workingCopyTypeId, + resource, + this.labelService.getUriBasenameLabel(resource), + this.modelFactory + ) as unknown as IFileWorkingCopy; + workingCopyResolve = workingCopy.resolve(options); - this.registerWorkingCopy(newWorkingCopy); + this.registerWorkingCopy(workingCopy); } // Store pending resolve to avoid race conditions diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 02ed75d2d8e..670517c603f 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -7,7 +7,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -35,13 +35,18 @@ export interface IUntitledFileWorkingCopyModelContentChangedEvent extends IFileW readonly isEmpty: boolean; } -export interface IUntitledFileWorkingCopy extends IWorkingCopy { +export interface IUntitledFileWorkingCopy extends IWorkingCopy, IDisposable { /** * Emits an event when this untitled model is reverted. */ readonly onDidRevert: Event; + /** + * An event for when the file working copy has been disposed. + */ + readonly onWillDispose: Event; + /** * Provides access to the underlying model of this untitled * file based working copy. As long as the untitled file working @@ -86,6 +91,9 @@ export class UntitledFileWorkingCopy ex private readonly _onDidRevert = this._register(new Emitter()); readonly onDidRevert = this._onDidRevert.event; + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + //#endregion constructor( @@ -272,6 +280,8 @@ export class UntitledFileWorkingCopy ex override dispose(): void { this.trace('[untitled file working copy] dispose()'); + this._onWillDispose.fire(); + super.dispose(); } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts new file mode 100644 index 00000000000..3a5c980a50d --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -0,0 +1,308 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { Promises } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IFileService } from 'vs/platform/files/common/files'; + +export interface INewUntitledFileWorkingCopyOptions { + + /** + * Initial value of the untitled file working copy. + * + * Note: An untitled file working copy with initial + * value is dirty right from the beginning. + */ + initialValue?: VSBufferReadableStream; +} + +export interface INewUntitledFileWorkingCopyWithAssociatedResourceOptions extends INewUntitledFileWorkingCopyOptions { + + /** + * Resource components to associate with the untitled file working copy. + * When saving, the associated components will be used and the user + * is not being asked to provide a file path. + * + * Note: currently it is not possible to specify the `scheme` to use. The + * untitled file working copy will saved to the default local or remote resource. + */ + associatedResource: { authority: string; path: string; query: string; fragment: string; } +} + +export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFileWorkingCopyOptions { + + /** + * A resource to identify the untitled file working copy + * to create or return if already existing. + * + * Note: the resource will not be used unless the scheme is `untitled`. + */ + untitledResource: URI; +} + +/** + * The only one that should be dealing with `IUntitledFileWorkingCopy` and + * handle all operations that are working copy related, such as save/revert, + * backup and resolving. + */ +export interface IUntitledFileWorkingCopyManager extends IDisposable { + + /** + * An event for when a untitled file working copy changed it's dirty state. + */ + readonly onDidChangeDirty: Event>; + + /** + * An event for when a untitled file working copy is about to be disposed. + */ + readonly onWillDispose: Event>; + + /** + * Access to all known untitled file working copies within the manager. + */ + readonly workingCopies: readonly IUntitledFileWorkingCopy[]; + + /** + * Returns an existing untitled file working copy if already created before + * or `undefined` otherwise. + */ + get(resource: URI): IUntitledFileWorkingCopy | undefined; + + /** + * Resolves an untitled file working copy from the provided options. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Resolves an untitled file working copy from the provided options + * unless an existing working copy already exists with that resource. + */ + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + + /** + * Disposes all working copies of the manager and disposes the manager. This + * method is different from `dispose` in that it will unregister any working + * copy from the `IWorkingCopyService`. Since this impact things like backups, + * the method is `async` because it needs to trigger `save` for any dirty + * working copy to preserve the data. + * + * Callers should make sure to e.g. close any editors associated with the + * working copy. + */ + destroy(): Promise; +} + +type IInternalUntitledFileWorkingCopyOptions = IExistingUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions; + +export class UntitledFileWorkingCopyManager extends Disposable implements IUntitledFileWorkingCopyManager { + + //#region Events + + private readonly _onDidChangeDirty = this._register(new Emitter>()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onWillDispose = this._register(new Emitter>()); + readonly onWillDispose = this._onWillDispose.event; + + //#endregion + + private readonly mapResourceToWorkingCopy = new ResourceMap>(); + + constructor( + private readonly workingCopyTypeId: string, + private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILabelService private readonly labelService: ILabelService, + @ILogService private readonly logService: ILogService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @IFileService private readonly fileService: IFileService + ) { + super(); + } + + //#region Get / Get all + + get workingCopies(): IUntitledFileWorkingCopy[] { + return [...this.mapResourceToWorkingCopy.values()]; + } + + get(resource: URI): IUntitledFileWorkingCopy | undefined { + return this.mapResourceToWorkingCopy.get(resource); + } + + //#endregion + + //#region Resolve / Create + + async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { + const workingCopy = this.doCreateOrGet(options); + await workingCopy.resolve(); + + return workingCopy; + } + + private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { + const massagedOptions = this.massageOptions(options); + + // Return existing instance if asked for it + if (massagedOptions.untitledResource && this.mapResourceToWorkingCopy.has(massagedOptions.untitledResource)) { + return this.mapResourceToWorkingCopy.get(massagedOptions.untitledResource)!; + } + + // Create new instance otherwise + return this.doCreate(massagedOptions); + } + + private massageOptions(options: IInternalUntitledFileWorkingCopyOptions): IInternalUntitledFileWorkingCopyOptions { + const massagedOptions: IInternalUntitledFileWorkingCopyOptions = Object.create(null); + + // Figure out associated and untitled resource + if (options.associatedResource) { + massagedOptions.untitledResource = URI.from({ + scheme: Schemas.untitled, + authority: options.associatedResource.authority, + fragment: options.associatedResource.fragment, + path: options.associatedResource.path, + query: options.associatedResource.query + }); + massagedOptions.associatedResource = options.associatedResource; + } else { + if (options.untitledResource?.scheme === Schemas.untitled) { + massagedOptions.untitledResource = options.untitledResource; + } + } + + // Take over initial value + massagedOptions.initialValue = options.initialValue; + + return massagedOptions; + } + + private doCreate(options: IInternalUntitledFileWorkingCopyOptions): IUntitledFileWorkingCopy { + + // Create a new untitled resource if none is provided + let untitledResource = options.untitledResource; + if (!untitledResource) { + let counter = 1; + do { + untitledResource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` }); + counter++; + } while (this.mapResourceToWorkingCopy.has(untitledResource)); + } + + // Create new working copy with provided options + const workingCopy = this.instantiationService.createInstance( + UntitledFileWorkingCopy, + this.workingCopyTypeId, + untitledResource, + this.labelService.getUriBasenameLabel(untitledResource), + !!options.associatedResource, + options.initialValue, + this.modelFactory + ) as unknown as IUntitledFileWorkingCopy; + + this.registerWorkingCopy(workingCopy); + + return workingCopy; + } + + private registerWorkingCopy(workingCopy: IUntitledFileWorkingCopy): void { + + // Install working copy listeners + const listeners = new DisposableStore(); + listeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); + listeners.add(workingCopy.onWillDispose(() => this._onWillDispose.fire(workingCopy))); + + // Remove from cache on dispose + Event.once(workingCopy.onWillDispose)(() => { + + // Registry + this.mapResourceToWorkingCopy.delete(workingCopy.resource); + + // Listeners + listeners.dispose(); + }); + + // Add to cache + this.mapResourceToWorkingCopy.set(workingCopy.resource, workingCopy); + + // If the working copy is dirty right from the beginning, + // make sure to emit this as an event + if (workingCopy.isDirty()) { + this._onDidChangeDirty.fire(workingCopy); + } + } + + //#endregion + + //#region Lifecycle + + override dispose(): void { + super.dispose(); + + // Clear working copy caches + // + // Note: we are not explicitly disposing the working copies + // known to the manager because this can have unwanted side + // effects such as backups getting discarded once the working + // copy unregisters. We have an explicit `destroy` + // for that purpose (https://github.com/microsoft/vscode/pull/123555) + // + this.mapResourceToWorkingCopy.clear(); + } + + async destroy(): Promise { + const workingCopies = Array.from(this.mapResourceToWorkingCopy.values()); + + // Make sure all dirty working copies are saved to disk + try { + await Promises.settled(workingCopies.map(async workingCopy => { + if (workingCopy.isDirty()) { + await this.saveWithFallback(workingCopy); + } + })); + } catch (error) { + this.logService.error(error); + } + + // Dispose all working copies + dispose(workingCopies); + + // Finally dispose manager + this.dispose(); + } + + private async saveWithFallback(workingCopy: IUntitledFileWorkingCopy): Promise { + + // First try regular save + let saveFailed = false; + try { + await workingCopy.save(); + } catch (error) { + saveFailed = true; + } + + // Then fallback to backup if that exists + if (saveFailed || workingCopy.isDirty()) { + const backup = await this.workingCopyBackupService.resolve(workingCopy); + if (backup) { + await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); + } + } + + //#endregion + } +} From 103ba104a56f31a20fe5ee4694f89a86b6dc9365 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 17 May 2021 15:37:17 +0200 Subject: [PATCH 087/270] untitled file working copy - tests --- .../common/untitledFileWorkingCopy.ts | 6 +- .../common/untitledFileWorkingCopyManager.ts | 4 +- .../test/browser/fileWorkingCopy.test.ts | 8 + .../browser/untitledFileWorkingCopy.test.ts | 311 ++++++++++++++++++ .../untitledFileWorkingCopyManager.test.ts | 146 ++++++++ 5 files changed, 471 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts create mode 100644 src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 670517c603f..0d59f0fa23d 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -244,7 +244,7 @@ export class UntitledFileWorkingCopy ex //#region Save - save(options?: ISaveOptions): Promise { + async save(options?: ISaveOptions): Promise { this.trace('[untitled file working copy] save() - enter'); // TODO needs to extract the code for bringing up save dialog @@ -252,7 +252,9 @@ export class UntitledFileWorkingCopy ex // Also, who is disposing the untitled after save and open the // new editor? - throw new Error('Method not implemented.'); + await this.revert(); + + return true; } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 3a5c980a50d..3585096df18 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -38,7 +38,7 @@ export interface INewUntitledFileWorkingCopyWithAssociatedResourceOptions extend * Note: currently it is not possible to specify the `scheme` to use. The * untitled file working copy will saved to the default local or remote resource. */ - associatedResource: { authority: string; path: string; query: string; fragment: string; } + associatedResource: { authority?: string; path?: string; query?: string; fragment?: string; } } export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFileWorkingCopyOptions { @@ -145,7 +145,7 @@ export class UntitledFileWorkingCopyManager> { const workingCopy = this.doCreateOrGet(options); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index 3c0028841b7..47ab58bb6a7 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -112,6 +112,14 @@ suite('FileWorkingCopy', function () { workingCopy.dispose(); }); + test('registers with working copy service', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + + workingCopy.dispose(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + }); + test('requires good file system URI', async () => { assert.throws(() => createWorkingCopy(URI.from({ scheme: 'unknown', path: 'somePath' }))); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts new file mode 100644 index 00000000000..8c7da14566a --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -0,0 +1,311 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { VSBufferReadableStream, newWriteableBufferStream, VSBuffer, streamToBuffer, bufferToStream } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/resources'; +import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +export class TestUntitledFileWorkingCopyModel extends Disposable implements IUntitledFileWorkingCopyModel { + + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + constructor(readonly resource: URI, public contents: string) { + super(); + } + + fireContentChangeEvent(event: IUntitledFileWorkingCopyModelContentChangedEvent): void { + this._onDidChangeContent.fire(event); + } + + updateContents(newContents: string): void { + this.doUpdate(newContents); + } + + private throwOnSnapshot = false; + setThrowOnSnapshot(): void { + this.throwOnSnapshot = true; + } + + async snapshot(token: CancellationToken): Promise { + if (this.throwOnSnapshot) { + throw new Error('Fail'); + } + + const stream = newWriteableBufferStream(); + stream.end(VSBuffer.fromString(this.contents)); + + return stream; + } + + async update(contents: VSBufferReadableStream, token: CancellationToken): Promise { + this.doUpdate((await streamToBuffer(contents)).toString()); + } + + private doUpdate(newContents: string): void { + this.contents = newContents; + + this.versionId++; + + this._onDidChangeContent.fire({ isRedoing: false, isUndoing: false, isEmpty: newContents.length === 0 }); + } + + versionId = 0; + + pushedStackElement = false; + + pushStackElement(): void { + this.pushedStackElement = true; + } + + override dispose(): void { + this._onWillDispose.fire(); + + super.dispose(); + } +} + +export class TestUntitledFileWorkingCopyModelFactory implements IUntitledFileWorkingCopyModelFactory { + + async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise { + return new TestUntitledFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString()); + } +} + +suite('UntitledFileWorkingCopy', () => { + + const factory = new TestUntitledFileWorkingCopyModelFactory(); + + let resource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + let workingCopy: UntitledFileWorkingCopy; + + function createWorkingCopy(uri: URI = resource, hasAssociatedFilePath = false, initialValue = '') { + return new UntitledFileWorkingCopy( + 'testUntitledWorkingCopyType', + uri, + basename(uri), + hasAssociatedFilePath, + initialValue.length > 0 ? bufferToStream(VSBuffer.fromString(initialValue)) : undefined, + factory, + accessor.workingCopyService, + accessor.workingCopyBackupService, + accessor.logService + ); + } + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + workingCopy = createWorkingCopy(); + }); + + teardown(() => { + workingCopy.dispose(); + }); + + test('registers with working copy service', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + + workingCopy.dispose(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + }); + + test('requires good untitled URI', async () => { + assert.throws(() => createWorkingCopy(URI.from({ scheme: 'unknown', path: 'somePath' }))); + }); + + test('dirty', async () => { + assert.strictEqual(workingCopy.isDirty(), false); + + let changeDirtyCounter = 0; + workingCopy.onDidChangeDirty(() => { + changeDirtyCounter++; + }); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + assert.strictEqual(workingCopy.isResolved(), true); + + // Dirty from: Model content change + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(contentChangeCounter, 1); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(changeDirtyCounter, 1); + + await workingCopy.save(); + + assert.strictEqual(workingCopy.isDirty(), false); + assert.strictEqual(changeDirtyCounter, 2); + }); + + test('dirty - cleared when content event signals isEmpty', async () => { + assert.strictEqual(workingCopy.isDirty(), false); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + workingCopy.model?.fireContentChangeEvent({ isEmpty: true, isUndoing: false, isRedoing: false }); + + assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('dirty - not cleared when content event signals isEmpty when associated resource', async () => { + workingCopy.dispose(); + workingCopy = createWorkingCopy(resource, true); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + workingCopy.model?.fireContentChangeEvent({ isEmpty: true, isUndoing: false, isRedoing: false }); + + assert.strictEqual(workingCopy.isDirty(), true); + }); + + test('revert', async () => { + let revertCounter = 0; + workingCopy.onDidRevert(() => { + revertCounter++; + }); + + let disposeCounter = 0; + workingCopy.onWillDispose(() => { + disposeCounter++; + }); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello dirty'); + assert.strictEqual(workingCopy.isDirty(), true); + + await workingCopy.revert(); + + assert.strictEqual(revertCounter, 1); + assert.strictEqual(disposeCounter, 1); + assert.strictEqual(workingCopy.isDirty(), false); + }); + + test('dispose', async () => { + let disposeCounter = 0; + workingCopy.onWillDispose(() => { + disposeCounter++; + }); + + await workingCopy.resolve(); + workingCopy.dispose(); + + assert.strictEqual(disposeCounter, 1); + }); + + test('backup', async () => { + assert.strictEqual((await workingCopy.backup(CancellationToken.None)).content, undefined); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('Hello Backup'); + const backup = await workingCopy.backup(CancellationToken.None); + + let backupContents: string | undefined = undefined; + if (isReadableStream(backup.content)) { + backupContents = (await consumeStream(backup.content, chunks => VSBuffer.concat(chunks))).toString(); + } else if (backup.content) { + backupContents = consumeReadable(backup.content, chunks => VSBuffer.concat(chunks)).toString(); + } + + assert.strictEqual(backupContents, 'Hello Backup'); + }); + + test('resolve - without contents', async () => { + assert.strictEqual(workingCopy.isResolved(), false); + assert.strictEqual(workingCopy.hasAssociatedFilePath, false); + assert.strictEqual(workingCopy.model, undefined); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isResolved(), true); + assert.ok(workingCopy.model); + }); + + test('resolve - with initial contents', async () => { + workingCopy.dispose(); + + workingCopy = createWorkingCopy(resource, false, 'Hello Initial'); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.model?.contents, 'Hello Initial'); + assert.strictEqual(contentChangeCounter, 1); + + workingCopy.model.updateContents('Changed contents'); + + await workingCopy.resolve(); // second resolve should be ignored + assert.strictEqual(workingCopy.model?.contents, 'Changed contents'); + }); + + test('resolve - with associated resource', async () => { + workingCopy.dispose(); + workingCopy = createWorkingCopy(resource, true); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.hasAssociatedFilePath, true); + }); + + test('resolve - with backup', async () => { + await workingCopy.resolve(); + workingCopy.model?.updateContents('Hello Backup'); + + const backup = await workingCopy.backup(CancellationToken.None); + await accessor.workingCopyBackupService.backup(workingCopy, backup.content, undefined, backup.meta); + + assert.strictEqual(accessor.workingCopyBackupService.hasBackupSync(workingCopy), true); + + workingCopy.dispose(); + + workingCopy = createWorkingCopy(); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(workingCopy.model?.contents, 'Hello Backup'); + assert.strictEqual(contentChangeCounter, 1); + }); +}); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts new file mode 100644 index 00000000000..417fad0cfbe --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; +import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('UntitledFileWorkingCopyManager', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + let manager: IUntitledFileWorkingCopyManager; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + const factory = new TestUntitledFileWorkingCopyModelFactory(); + manager = new UntitledFileWorkingCopyManager('testUntitledWorkingCopyType', factory, instantiationService, accessor.labelService, accessor.logService, accessor.workingCopyBackupService, accessor.fileService); + }); + + teardown(() => { + manager.dispose(); + }); + + test('basics', async () => { + let disposeCounter = 0; + manager.onWillDispose(e => { + disposeCounter++; + }); + + let dirtyCounter = 0; + manager.onDidChangeDirty(e => { + dirtyCounter++; + }); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.workingCopies.length, 0); + + assert.strictEqual(manager.get(URI.file('/some/invalidPath')), undefined); + assert.strictEqual(manager.get(URI.file('/some/invalidPath').with({ scheme: Schemas.untitled })), undefined); + + const workingCopy1 = await manager.resolve(); + const workingCopy2 = await manager.resolve(); + + assert.strictEqual(manager.get(workingCopy1.resource), workingCopy1); + assert.strictEqual(manager.get(workingCopy2.resource), workingCopy2); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); + assert.strictEqual(manager.workingCopies.length, 2); + + assert.notStrictEqual(workingCopy1.resource.toString(), workingCopy2.resource.toString()); + + for (const workingCopy of [workingCopy1, workingCopy2]) { + assert.strictEqual(workingCopy.capabilities, WorkingCopyCapabilities.Untitled); + assert.strictEqual(workingCopy.isDirty(), false); + assert.ok(workingCopy.model); + } + + workingCopy1.model?.updateContents('Hello World'); + + assert.strictEqual(workingCopy1.isDirty(), true); + assert.strictEqual(dirtyCounter, 1); + + workingCopy1.model?.updateContents(''); // change to empty clears dirty flag + assert.strictEqual(workingCopy1.isDirty(), false); + assert.strictEqual(dirtyCounter, 2); + + workingCopy2.model?.fireContentChangeEvent({ isEmpty: false, isRedoing: false, isUndoing: false }); + assert.strictEqual(workingCopy2.isDirty(), true); + assert.strictEqual(dirtyCounter, 3); + + workingCopy1.dispose(); + + assert.strictEqual(manager.workingCopies.length, 1); + assert.strictEqual(manager.get(workingCopy1.resource), undefined); + + workingCopy2.dispose(); + + assert.strictEqual(manager.workingCopies.length, 0); + assert.strictEqual(manager.get(workingCopy2.resource), undefined); + + assert.strictEqual(disposeCounter, 2); + }); + + test('resolve - with initial value', async () => { + let dirtyCounter = 0; + manager.onDidChangeDirty(e => { + dirtyCounter++; + }); + + const workingCopy = await manager.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello World')) }); + + assert.strictEqual(workingCopy.isDirty(), true); + assert.strictEqual(dirtyCounter, 1); + assert.strictEqual(workingCopy.model?.contents, 'Hello World'); + + workingCopy.dispose(); + }); + + test('resolve - existing', async () => { + const workingCopy1 = await manager.resolve(); + + const workingCopy2 = await manager.resolve({ untitledResource: workingCopy1.resource }); + assert.strictEqual(workingCopy1, workingCopy2); + + const workingCopy3 = await manager.resolve({ untitledResource: URI.file('/invalid/untitled') }); + assert.strictEqual(workingCopy3.resource.scheme, Schemas.untitled); + + workingCopy1.dispose(); + workingCopy2.dispose(); + workingCopy3.dispose(); + }); + + test('resolve - with associated resource', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + + assert.strictEqual(workingCopy.hasAssociatedFilePath, true); + assert.strictEqual(workingCopy.resource.path, '/some/associated.txt'); + }); + + test('destroy', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + + await manager.resolve(); + await manager.resolve(); + await manager.resolve(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); + assert.strictEqual(manager.workingCopies.length, 3); + + await manager.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.workingCopies.length, 0); + }); +}); From 44dec56af8c59e75436a596a8b25651e774c6a4d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 17 May 2021 16:32:14 +0200 Subject: [PATCH 088/270] untitled file working copy - :lipstick: --- .../workingCopy/common/untitledFileWorkingCopyManager.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 3585096df18..674cca7295a 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -158,8 +158,11 @@ export class UntitledFileWorkingCopyManager Date: Tue, 18 May 2021 07:58:10 +0200 Subject: [PATCH 089/270] untitled file working copy - extract reusable interfaces --- .../common/abstractFileWorkingCopy.ts | 99 +++++++++++++++++++ .../workingCopy/common/fileWorkingCopy.ts | 82 ++------------- .../common/untitledFileWorkingCopy.ts | 48 ++++----- .../browser/untitledFileWorkingCopy.test.ts | 6 +- .../untitledFileWorkingCopyManager.test.ts | 2 +- 5 files changed, 129 insertions(+), 108 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts new file mode 100644 index 00000000000..36b4f2ac181 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { URI } from 'vs/base/common/uri'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; + +export interface IBaseFileWorkingCopyModelFactory { + + /** + * Create a model from the given content under the provided resource. + * + * @param resource the `URI` of the model + * @param contents the content of the model to create it + * @param token support for cancellation + */ + createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise; +} + +/** + * A generic file working copy model to be reused by untitled + * and titled file working copies. + */ +export interface IBaseFileWorkingCopyModel extends IDisposable { + + /** + * This event signals ANY changes to the contents, for example: + * - through the user typing into the editor + * - from API usage (e.g. bulk edits) + * - when `IBaseFileWorkingCopyModel#update` is invoked with contents + * that are different from the current contents + * + * The file working copy will listen to these changes and may mark + * the working copy as dirty whenever this event fires. + * + * Note: ONLY report changes to the model but not the underlying + * file. The file working copy is tracking changes to the file + * automatically. + */ + readonly onDidChangeContent: Event; + + /** + * An event emitted right before disposing the model. + */ + readonly onWillDispose: Event; + + /** + * Snapshots the model's current content for writing. This must include + * any changes that were made to the model that are in memory. + * + * @param token support for cancellation + */ + snapshot(token: CancellationToken): Promise; + + /** + * Updates the model with the provided contents. The implementation should + * behave in a similar fashion as `IBaseFileWorkingCopyModelFactory#createModel` + * except that here the model already exists and just needs to update to + * the provided contents. + * + * Note: it is expected that the model fires a `onDidChangeContent` event + * as part of the update. + * + * @param the contents to use for the model + * @param token support for cancellation + */ + update(contents: VSBufferReadableStream, token: CancellationToken): Promise; +} + +export interface IBaseFileWorkingCopy extends IWorkingCopy, IDisposable { + + /** + * An event for when the file working copy has been reverted. + */ + readonly onDidRevert: Event; + + /** + * An event for when the file working copy has been disposed. + */ + readonly onWillDispose: Event; + + /** + * Provides access to the underlying model of this file + * based working copy. As long as the file working copy + * has not been resolved, the model is `undefined`. + */ + readonly model: T | undefined; + + /** + * Resolves the file working copy and thus makes the `model` + * available. + */ + resolve(): Promise; +} diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index ba40ccd1df1..5a7e140a6d8 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -7,7 +7,6 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { ETAG_DISABLED, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent, IWriteFileOptions } from 'vs/platform/files/common/files'; import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -28,23 +27,12 @@ import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/com import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; import { IResourceWorkingCopy, ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; +import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel, IBaseFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; -export interface IFileWorkingCopyModelFactory { - - /** - * Asks the file working copy delegate to create a model from the given - * content under the provided resource. The content may originate from - * different sources depending on context: - * - from a backup if that exists - * - from the underlying file resource - * - passed in from the caller - * - * @param resource the `URI` of the model - * @param contents the content of the model to create it - * @param token support for cancellation - */ - createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise; -} +/** + * File specific working copy model factory. + */ +export interface IFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } /** * The underlying model of a file working copy provides some @@ -52,30 +40,10 @@ export interface IFileWorkingCopyModelFactory { * typically only available after the working copy has been * resolved via it's `resolve()` method. */ -export interface IFileWorkingCopyModel extends IDisposable { +export interface IFileWorkingCopyModel extends IBaseFileWorkingCopyModel { - /** - * This event signals ANY changes to the contents of the file - * working copy model, for example: - * - through the user typing into the editor - * - from API usage (e.g. bulk edits) - * - when `IFileWorkingCopyModel#update` is invoked with contents - * that are different from the current contents - * - * The file working copy will listen to these changes and mark - * the working copy as dirty whenever this event fires. - * - * Note: ONLY report changes to the model but not the underlying - * file. The file working copy is tracking changes to the file - * automatically. - */ readonly onDidChangeContent: Event; - /** - * An event emitted right before disposing the model. - */ - readonly onWillDispose: Event; - /** * A version ID of the model. If a `onDidChangeContent` is fired * from the model and the last known saved `versionId` matches @@ -91,28 +59,6 @@ export interface IFileWorkingCopyModel extends IDisposable { */ readonly versionId: unknown; - /** - * Snapshots the model's current content for writing. This must include - * any changes that were made to the model that are in memory. - * - * @param token support for cancellation - */ - snapshot(token: CancellationToken): Promise; - - /** - * Updates the model with the provided contents. The implementation should - * behave in a similar fashion as `IFileWorkingCopyModelFactory#createModel` - * except that here the model already exists and just needs to update to - * the provided contents. - * - * Note: it is expected that the model fires a `onDidChangeContent` event - * as part of the update. - * - * @param the contents to use for the model - * @param token support for cancellation - */ - update(contents: VSBufferReadableStream, token: CancellationToken): Promise; - /** * Close the current undo-redo element. This offers a way * to create an undo/redo stop point. @@ -143,7 +89,7 @@ export interface IFileWorkingCopyModelContentChangedEvent { * of functionality can be built on top, such as saving in * a secure way to prevent data loss. */ -export interface IFileWorkingCopy extends IResourceWorkingCopy { +export interface IFileWorkingCopy extends IResourceWorkingCopy, IBaseFileWorkingCopy { /** * An event for when a file working copy was resolved. @@ -160,23 +106,11 @@ export interface IFileWorkingCopy extends IReso */ readonly onDidSaveError: Event; - /** - * An event for when the file working copy was reverted. - */ - readonly onDidRevert: Event; - /** * An event for when the readonly state of the file working copy changes. */ readonly onDidChangeReadonly: Event; - /** - * Provides access to the underlying model of this file - * based working copy. As long as the file working copy - * has not been resolved, the model is `undefined`. - */ - readonly model: T | undefined; - /** * Resolves a file working copy. */ @@ -217,7 +151,7 @@ export interface IFileWorkingCopy extends IReso export interface IResolvedFileWorkingCopy extends IFileWorkingCopy { /** - * A resolved file working copy has a resolved model `T`. + * A resolved file working copy has a resolved model. */ readonly model: T; } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 0d59f0fa23d..77a33c99997 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -5,9 +5,9 @@ import { Event, Emitter } from 'vs/base/common/event'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; -import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel, IBaseFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -18,13 +18,23 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { emptyStream } from 'vs/base/common/stream'; -export interface IUntitledFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } +/** + * Untitled file specific working copy model factory. + */ +export interface IUntitledFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } + +/** + * The underlying model of a untitled file working copy provides + * some methods for the untitled file working copy to function. + * The model is typically only available after the working copy + * has been resolved via it's `resolve()` method. + */ +export interface IUntitledFileWorkingCopyModel extends IBaseFileWorkingCopyModel { -export interface IUntitledFileWorkingCopyModel extends IFileWorkingCopyModel { readonly onDidChangeContent: Event; } -export interface IUntitledFileWorkingCopyModelContentChangedEvent extends IFileWorkingCopyModelContentChangedEvent { +export interface IUntitledFileWorkingCopyModelContentChangedEvent { /** * Flag that indicates that the content change @@ -35,40 +45,18 @@ export interface IUntitledFileWorkingCopyModelContentChangedEvent extends IFileW readonly isEmpty: boolean; } -export interface IUntitledFileWorkingCopy extends IWorkingCopy, IDisposable { - - /** - * Emits an event when this untitled model is reverted. - */ - readonly onDidRevert: Event; - - /** - * An event for when the file working copy has been disposed. - */ - readonly onWillDispose: Event; - - /** - * Provides access to the underlying model of this untitled - * file based working copy. As long as the untitled file working - * copy has not been resolved, the model is `undefined`. - */ - readonly model: T | undefined; +export interface IUntitledFileWorkingCopy extends IBaseFileWorkingCopy { /** * Whether this untitled file working copy model has an associated file path. */ readonly hasAssociatedFilePath: boolean; - - /** - * Resolves an untitled file working copy. - */ - resolve(): Promise; } export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { /** - * A resolved untitled file working copy has a resolved model `T`. + * A resolved untitled file working copy has a resolved model. */ readonly model: T; } diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index 8c7da14566a..d4ed35dd986 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -61,7 +61,7 @@ export class TestUntitledFileWorkingCopyModel extends Disposable implements IUnt this.versionId++; - this._onDidChangeContent.fire({ isRedoing: false, isUndoing: false, isEmpty: newContents.length === 0 }); + this._onDidChangeContent.fire({ isEmpty: newContents.length === 0 }); } versionId = 0; @@ -169,7 +169,7 @@ suite('UntitledFileWorkingCopy', () => { workingCopy.model?.updateContents('hello dirty'); assert.strictEqual(workingCopy.isDirty(), true); - workingCopy.model?.fireContentChangeEvent({ isEmpty: true, isUndoing: false, isRedoing: false }); + workingCopy.model?.fireContentChangeEvent({ isEmpty: true }); assert.strictEqual(workingCopy.isDirty(), false); }); @@ -183,7 +183,7 @@ suite('UntitledFileWorkingCopy', () => { workingCopy.model?.updateContents('hello dirty'); assert.strictEqual(workingCopy.isDirty(), true); - workingCopy.model?.fireContentChangeEvent({ isEmpty: true, isUndoing: false, isRedoing: false }); + workingCopy.model?.fireContentChangeEvent({ isEmpty: true }); assert.strictEqual(workingCopy.isDirty(), true); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 417fad0cfbe..1dcc6163294 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -75,7 +75,7 @@ suite('UntitledFileWorkingCopyManager', () => { assert.strictEqual(workingCopy1.isDirty(), false); assert.strictEqual(dirtyCounter, 2); - workingCopy2.model?.fireContentChangeEvent({ isEmpty: false, isRedoing: false, isUndoing: false }); + workingCopy2.model?.fireContentChangeEvent({ isEmpty: false }); assert.strictEqual(workingCopy2.isDirty(), true); assert.strictEqual(dirtyCounter, 3); From a3ee06b3e030dbbfc15c1f9d87fc9938a764429f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 08:30:01 +0200 Subject: [PATCH 090/270] untitled file working copy - extract common super type for manager --- .../common/abstractFileWorkingCopyManager.ts | 133 +++++++++++++ .../common/fileWorkingCopyManager.ts | 109 ++--------- .../common/untitledFileWorkingCopyManager.ts | 178 ++++-------------- 3 files changed, 187 insertions(+), 233 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts new file mode 100644 index 00000000000..42773b88bf1 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { Promises } from 'vs/base/common/async'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; + +export interface IBaseFileWorkingCopyManager> extends IDisposable { + + /** + * Access to all known file working copies within the manager. + */ + readonly workingCopies: readonly W[]; + + /** + * Returns the file working copy for the provided resource + * or `undefined` if none. + */ + get(resource: URI): W | undefined; + + /** + * Disposes all working copies of the manager and disposes the manager. This + * method is different from `dispose` in that it will unregister any working + * copy from the `IWorkingCopyService`. Since this impact things like backups, + * the method is `async` because it needs to trigger `save` for any dirty + * working copy to preserve the data. + * + * Callers should make sure to e.g. close any editors associated with the + * working copy. + */ + destroy(): Promise; +} + +export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { + + private readonly mapResourceToWorkingCopy = new ResourceMap(); + + constructor( + @IFileService protected readonly fileService: IFileService, + @ILogService protected readonly logService: ILogService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService + ) { + super(); + } + + protected has(resource: URI): boolean { + return this.mapResourceToWorkingCopy.has(resource); + } + + protected add(resource: URI, workingCopy: W): void { + this.mapResourceToWorkingCopy.set(resource, workingCopy); + } + + protected remove(resource: URI): void { + this.mapResourceToWorkingCopy.delete(resource); + } + + //#region Get / Get all + + get workingCopies(): W[] { + return [...this.mapResourceToWorkingCopy.values()]; + } + + get(resource: URI): W | undefined { + return this.mapResourceToWorkingCopy.get(resource); + } + + //#endregion + + //#region Lifecycle + + override dispose(): void { + super.dispose(); + + // Clear working copy caches + // + // Note: we are not explicitly disposing the working copies + // known to the manager because this can have unwanted side + // effects such as backups getting discarded once the working + // copy unregisters. We have an explicit `destroy` + // for that purpose (https://github.com/microsoft/vscode/pull/123555) + // + this.mapResourceToWorkingCopy.clear(); + } + + async destroy(): Promise { + + // Make sure all dirty working copies are saved to disk + try { + await Promises.settled(this.workingCopies.map(async workingCopy => { + if (workingCopy.isDirty()) { + await this.saveWithFallback(workingCopy); + } + })); + } catch (error) { + this.logService.error(error); + } + + // Dispose all working copies + dispose(this.mapResourceToWorkingCopy.values()); + + // Finally dispose manager + this.dispose(); + } + + private async saveWithFallback(workingCopy: W): Promise { + + // First try regular save + let saveFailed = false; + try { + await workingCopy.save(); + } catch (error) { + saveFailed = true; + } + + // Then fallback to backup if that exists + if (saveFailed || workingCopy.isDirty()) { + const backup = await this.workingCopyBackupService.resolve(workingCopy); + if (backup) { + await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); + } + } + } + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 72a7e3f3c5e..c1bc7d08104 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopySaveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { SaveReason } from 'vs/workbench/common/editor'; @@ -22,13 +22,14 @@ import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/serv import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; /** * The only one that should be dealing with `IFileWorkingCopy` and handle all * operations that are working copy related, such as save/revert, backup * and resolving. */ -export interface IFileWorkingCopyManager extends IDisposable { +export interface IFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { /** * An event for when a file working copy was created. @@ -60,17 +61,6 @@ export interface IFileWorkingCopyManager extend */ readonly onDidRevert: Event>; - /** - * Access to all known file working copies within the manager. - */ - readonly workingCopies: readonly IFileWorkingCopy[]; - - /** - * Returns the file working copy for the provided resource - * or `undefined` if none. - */ - get(resource: URI): IFileWorkingCopy | undefined; - /** * Allows to resolve a file working copy. If the manager already knows * about a file working copy with the same `URI`, it will return that @@ -119,18 +109,6 @@ export interface IFileWorkingCopyManager extend * it is dirty. Once the promise is settled, it is safe to dispose. */ canDispose(workingCopy: IFileWorkingCopy): true | Promise; - - /** - * Disposes all working copies of the manager and disposes the manager. This - * method is different from `dispose` in that it will unregister any working - * copy from the `IWorkingCopyService`. Since this impact things like backups, - * the method is `async` because it needs to trigger `save` for any dirty - * working copy to preserve the data. - * - * Callers should make sure to e.g. close any editors associated with the - * working copy. - */ - destroy(): Promise; } export interface IFileWorkingCopySaveEvent { @@ -180,7 +158,7 @@ export interface IFileWorkingCopySaveAsOptions extends IFileWorkingCopySaveOptio suggestedTarget?: URI; } -export class FileWorkingCopyManager extends Disposable implements IFileWorkingCopyManager { +export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events @@ -204,7 +182,6 @@ export class FileWorkingCopyManager extends Dis //#endregion - private readonly mapResourceToWorkingCopy = new ResourceMap>(); private readonly mapResourceToWorkingCopyListeners = new ResourceMap(); private readonly mapResourceToDisposeListener = new ResourceMap(); private readonly mapResourceToPendingWorkingCopyResolve = new ResourceMap>(); @@ -214,17 +191,17 @@ export class FileWorkingCopyManager extends Dis constructor( private readonly workingCopyTypeId: string, private readonly modelFactory: IFileWorkingCopyModelFactory, - @IFileService private readonly fileService: IFileService, + @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ILogService private readonly logService: ILogService, + @ILogService logService: ILogService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { - super(); + super(fileService, logService, workingCopyBackupService); this.registerListeners(); } @@ -411,18 +388,6 @@ export class FileWorkingCopyManager extends Dis //#endregion - //#region Get / Get all - - get workingCopies(): IFileWorkingCopy[] { - return [...this.mapResourceToWorkingCopy.values()]; - } - - get(resource: URI): IFileWorkingCopy | undefined { - return this.mapResourceToWorkingCopy.get(resource); - } - - //#endregion - //#region Resolve async resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise> { @@ -554,8 +519,8 @@ export class FileWorkingCopyManager extends Dis this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); } - private add(resource: URI, workingCopy: IFileWorkingCopy): void { - const knownWorkingCopy = this.mapResourceToWorkingCopy.get(resource); + protected override add(resource: URI, workingCopy: IFileWorkingCopy): void { + const knownWorkingCopy = this.get(resource); if (knownWorkingCopy === workingCopy) { return; // already cached } @@ -567,12 +532,12 @@ export class FileWorkingCopyManager extends Dis } // Store in cache but remove when working copy gets disposed - this.mapResourceToWorkingCopy.set(resource, workingCopy); + super.add(resource, workingCopy); this.mapResourceToDisposeListener.set(resource, workingCopy.onWillDispose(() => this.remove(resource))); } - private remove(resource: URI): void { - this.mapResourceToWorkingCopy.delete(resource); + protected override remove(resource: URI): void { + super.remove(resource); const disposeListener = this.mapResourceToDisposeListener.get(resource); if (disposeListener) { @@ -723,15 +688,6 @@ export class FileWorkingCopyManager extends Dis override dispose(): void { super.dispose(); - // Clear working copy caches - // - // Note: we are not explicitly disposing the working copies - // known to the manager because this can have unwanted side - // effects such as backups getting discarded once the working - // copy unregisters. We have an explicit `destroy` - // for that purpose (https://github.com/microsoft/vscode/pull/123555) - // - this.mapResourceToWorkingCopy.clear(); this.mapResourceToPendingWorkingCopyResolve.clear(); // Dispose the dispose listeners @@ -743,44 +699,5 @@ export class FileWorkingCopyManager extends Dis this.mapResourceToWorkingCopyListeners.clear(); } - async destroy(): Promise { - - // Make sure all dirty working copies are saved to disk - try { - await Promises.settled(this.workingCopies.map(async workingCopy => { - if (workingCopy.isDirty()) { - await this.saveWithFallback(workingCopy); - } - })); - } catch (error) { - this.logService.error(error); - } - - // Dispose all working copies - dispose(this.mapResourceToWorkingCopy.values()); - - // Finally dispose manager - this.dispose(); - } - - private async saveWithFallback(workingCopy: IFileWorkingCopy): Promise { - - // First try regular save - let saveFailed = false; - try { - await workingCopy.save(); - } catch (error) { - saveFailed = true; - } - - // Then fallback to backup if that exists - if (saveFailed || workingCopy.isDirty()) { - const backup = await this.workingCopyBackupService.resolve(workingCopy); - if (backup) { - await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); - } - } - } - //#endregion } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 674cca7295a..3b2e2d4e9d4 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -4,18 +4,47 @@ *--------------------------------------------------------------------------------------------*/ import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { Event, Emitter } from 'vs/base/common/event'; -import { ResourceMap } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { Promises } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService } from 'vs/platform/files/common/files'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; + +/** + * The only one that should be dealing with `IUntitledFileWorkingCopy` and + * handle all operations that are working copy related, such as save/revert, + * backup and resolving. + */ +export interface IUntitledFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { + + /** + * An event for when a untitled file working copy changed it's dirty state. + */ + readonly onDidChangeDirty: Event>; + + /** + * An event for when a untitled file working copy is about to be disposed. + */ + readonly onWillDispose: Event>; + + /** + * Resolves an untitled file working copy from the provided options. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Resolves an untitled file working copy from the provided options + * unless an existing working copy already exists with that resource. + */ + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; +} export interface INewUntitledFileWorkingCopyOptions { @@ -52,62 +81,9 @@ export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFil untitledResource: URI; } -/** - * The only one that should be dealing with `IUntitledFileWorkingCopy` and - * handle all operations that are working copy related, such as save/revert, - * backup and resolving. - */ -export interface IUntitledFileWorkingCopyManager extends IDisposable { - - /** - * An event for when a untitled file working copy changed it's dirty state. - */ - readonly onDidChangeDirty: Event>; - - /** - * An event for when a untitled file working copy is about to be disposed. - */ - readonly onWillDispose: Event>; - - /** - * Access to all known untitled file working copies within the manager. - */ - readonly workingCopies: readonly IUntitledFileWorkingCopy[]; - - /** - * Returns an existing untitled file working copy if already created before - * or `undefined` otherwise. - */ - get(resource: URI): IUntitledFileWorkingCopy | undefined; - - /** - * Resolves an untitled file working copy from the provided options. - */ - resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; - resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; - - /** - * Resolves an untitled file working copy from the provided options - * unless an existing working copy already exists with that resource. - */ - resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; - - /** - * Disposes all working copies of the manager and disposes the manager. This - * method is different from `dispose` in that it will unregister any working - * copy from the `IWorkingCopyService`. Since this impact things like backups, - * the method is `async` because it needs to trigger `save` for any dirty - * working copy to preserve the data. - * - * Callers should make sure to e.g. close any editors associated with the - * working copy. - */ - destroy(): Promise; -} - type IInternalUntitledFileWorkingCopyOptions = IExistingUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions; -export class UntitledFileWorkingCopyManager extends Disposable implements IUntitledFileWorkingCopyManager { +export class UntitledFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IUntitledFileWorkingCopyManager { //#region Events @@ -119,32 +95,18 @@ export class UntitledFileWorkingCopyManager>(); - constructor( private readonly workingCopyTypeId: string, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, - @ILogService private readonly logService: ILogService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, - @IFileService private readonly fileService: IFileService + @ILogService logService: ILogService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, + @IFileService fileService: IFileService ) { - super(); + super(fileService, logService, workingCopyBackupService); } - //#region Get / Get all - - get workingCopies(): IUntitledFileWorkingCopy[] { - return [...this.mapResourceToWorkingCopy.values()]; - } - - get(resource: URI): IUntitledFileWorkingCopy | undefined { - return this.mapResourceToWorkingCopy.get(resource); - } - - //#endregion - //#region Resolve async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { @@ -159,7 +121,7 @@ export class UntitledFileWorkingCopyManager { // Registry - this.mapResourceToWorkingCopy.delete(workingCopy.resource); + this.remove(workingCopy.resource); // Listeners listeners.dispose(); }); // Add to cache - this.mapResourceToWorkingCopy.set(workingCopy.resource, workingCopy); + this.add(workingCopy.resource, workingCopy); // If the working copy is dirty right from the beginning, // make sure to emit this as an event @@ -250,62 +212,4 @@ export class UntitledFileWorkingCopyManager { - const workingCopies = Array.from(this.mapResourceToWorkingCopy.values()); - - // Make sure all dirty working copies are saved to disk - try { - await Promises.settled(workingCopies.map(async workingCopy => { - if (workingCopy.isDirty()) { - await this.saveWithFallback(workingCopy); - } - })); - } catch (error) { - this.logService.error(error); - } - - // Dispose all working copies - dispose(workingCopies); - - // Finally dispose manager - this.dispose(); - } - - private async saveWithFallback(workingCopy: IUntitledFileWorkingCopy): Promise { - - // First try regular save - let saveFailed = false; - try { - await workingCopy.save(); - } catch (error) { - saveFailed = true; - } - - // Then fallback to backup if that exists - if (saveFailed || workingCopy.isDirty()) { - const backup = await this.workingCopyBackupService.resolve(workingCopy); - if (backup) { - await this.fileService.writeFile(workingCopy.resource, backup.value, { unlock: true }); - } - } - - //#endregion - } } From 8173bd132fb14278e9b2573e44de61db0658d4a9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 10:22:03 +0200 Subject: [PATCH 091/270] untitled file working copy - add workingcopyservice#get --- .../services/textfile/browser/textFileService.ts | 2 +- .../services/workingCopy/common/workingCopyService.ts | 10 ++++++++++ .../workingCopy/test/common/workingCopyService.test.ts | 2 ++ src/vs/workbench/test/browser/workbenchTestServices.ts | 3 +++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 754fddd0d28..11c67c66e0b 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -457,7 +457,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // Otherwise try to suggest a path that can be saved let suggestedFilename: string | undefined = undefined; if (resource.scheme === Schemas.untitled) { - const model = this.untitledTextEditorService.get(resource); + const model = this.untitled.get(resource); if (model) { // Untitled with associated file path diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index c39b9872644..39a2fa4797e 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -97,6 +97,12 @@ export interface IWorkingCopyService { has(identifier: IWorkingCopyIdentifier): boolean; has(resource: URI): boolean; + /** + * Returns a working copy with the given identifier or `undefined` + * if no such working copy exists. + */ + get(identifier: IWorkingCopyIdentifier): IWorkingCopy | undefined; + //#endregion } @@ -192,6 +198,10 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic return this.mapResourceToWorkingCopies.get(resourceOrIdentifier.resource)?.has(resourceOrIdentifier.typeId) ?? false; } + get(identifier: IWorkingCopyIdentifier): IWorkingCopy | undefined { + return this.mapResourceToWorkingCopies.get(identifier.resource)?.get(identifier.typeId); + } + //#endregion diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index e06a9ba987e..01896acf907 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -35,6 +35,7 @@ suite('WorkingCopyService', () => { const resource1 = URI.file('/some/folder/file.txt'); assert.strictEqual(service.has(resource1), false); assert.strictEqual(service.has({ resource: resource1, typeId: 'testWorkingCopyType' }), false); + assert.strictEqual(service.get({ resource: resource1, typeId: 'testWorkingCopyType' }), undefined); const copy1 = new TestWorkingCopy(resource1); const unregister1 = service.registerWorkingCopy(copy1); @@ -46,6 +47,7 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.isDirty(resource1), false); assert.strictEqual(service.has(resource1), true); assert.strictEqual(service.has(copy1), true); + assert.strictEqual(service.get(copy1), copy1); assert.strictEqual(service.hasDirty, false); copy1.setDirty(true); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 6a23b7a9bf0..31a0a9caab2 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -261,8 +261,11 @@ export class TestServiceAccessor { @IModelService public modelService: ModelServiceImpl, @IFileService public fileService: TestFileService, @IFileDialogService public fileDialogService: TestFileDialogService, + @IDialogService public dialogService: IDialogService, @IWorkingCopyService public workingCopyService: IWorkingCopyService, @IEditorService public editorService: TestEditorService, + @IWorkbenchEnvironmentService public environmentService: IWorkbenchEnvironmentService, + @IPathService public pathService: IPathService, @IEditorGroupsService public editorGroupService: IEditorGroupsService, @IEditorOverrideService public editorOverrideService: IEditorOverrideService, @IModeService public modeService: IModeService, From 1edef157350d401c232afe80eb6af54b310ea3da Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 11:05:26 +0200 Subject: [PATCH 092/270] untitled file working copy - wire in save support --- .../notebook/common/notebookEditorModel.ts | 6 +- .../common/abstractFileWorkingCopy.ts | 13 + .../common/abstractFileWorkingCopyManager.ts | 253 +++++++++++++++++- .../common/fileWorkingCopyManager.ts | 129 ++------- .../common/untitledFileWorkingCopy.ts | 18 +- .../common/untitledFileWorkingCopyManager.ts | 37 ++- .../test/browser/fileWorkingCopy.test.ts | 2 +- .../browser/fileWorkingCopyManager.test.ts | 30 ++- .../browser/untitledFileWorkingCopy.test.ts | 1 + .../untitledFileWorkingCopyManager.test.ts | 17 +- 10 files changed, 373 insertions(+), 133 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 511261f2ada..03cb38b4b63 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -28,7 +28,7 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IFileWorkingCopyManager, IFileWorkingCopySaveAsOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { filter } from 'vs/base/common/objects'; //#region --- complex content provider @@ -491,8 +491,8 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE return this; } - async saveAs(target: URI, options?: IFileWorkingCopySaveAsOptions): Promise { - const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target, options); + async saveAs(target: URI): Promise { + const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target); if (!newWorkingCopy) { return undefined; } diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts index 36b4f2ac181..aa49c394e09 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts @@ -96,4 +96,17 @@ export interface IBaseFileWorkingCopy exten * available. */ resolve(): Promise; + + /** + * Whether we have a resolved model or not. + */ + isResolved(): this is IBaseResolvedFileWorkingCopy; +} + +export interface IBaseResolvedFileWorkingCopy extends IBaseFileWorkingCopy { + + /** + * A resolved file working copy has a resolved model. + */ + readonly model: T; } diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 42773b88bf1..e9021cb612d 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { Promises } from 'vs/base/common/async'; @@ -11,6 +12,19 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { FileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { IConfirmation, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { basename, dirname, isEqual, joinPath, toLocalResource } from 'vs/base/common/resources'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { isValidBasename } from 'vs/base/common/extpath'; +import { ISaveOptions } from 'vs/workbench/common/editor'; export interface IBaseFileWorkingCopyManager> extends IDisposable { @@ -38,14 +52,31 @@ export interface IBaseFileWorkingCopyManager; } +export interface IBaseFileWorkingCopySaveAsOptions extends ISaveOptions { + + /** + * Optional target resource to suggest to the user in case + * no taget resource is provided to save to. + */ + suggestedTarget?: URI; +} + export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { private readonly mapResourceToWorkingCopy = new ResourceMap(); constructor( + protected readonly workingCopyTypeId: string, @IFileService protected readonly fileService: IFileService, @ILogService protected readonly logService: ILogService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, + @IWorkingCopyFileService protected readonly workingCopyFileService: IWorkingCopyFileService, + @IDialogService private readonly dialogService: IDialogService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IPathService private readonly pathService: IPathService ) { super(); } @@ -72,6 +103,226 @@ export abstract class BaseFileWorkingCopyManager { + const workingCopy = this.get(resource); + + // Untitled + if (workingCopy instanceof UntitledFileWorkingCopy) { + let targetUri: URI | undefined; + + // Untitled with associated file path is taken as is + if (workingCopy.hasAssociatedFilePath) { + targetUri = await this.suggestSavePath(resource); + } + + // Otherwise ask user for a target path to save to + else { + targetUri = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(resource), options?.availableFileSystems); + } + + // Save as if target provided + if (targetUri) { + return this.saveAs(resource, targetUri, options); + } + } + + // File + else if (workingCopy) { + const success = await workingCopy.save(options); + if (success) { + return workingCopy; + } + } + + return undefined; + } + + async saveAs(source: URI, target?: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise { + + // Get to target resource + if (!target) { + target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + } + + if (!target) { + return; // user canceled + } + + // Just save if target is same as working copies own resource + if (isEqual(source, target)) { + return this.save(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + } + + // If the target is different but of same identity, we + // move the source to the target, knowing that the + // underlying file system cannot have both and then save. + // However, this will only work if the source exists + // and is not orphaned, so we need to check that too. + if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) { + await this.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); + + // At this point we don't know whether we have a + // working copy for the source or the target URI so we + // simply try to save with both resources. + let targetWorkingCopy = await this.save(source, options); + if (!targetWorkingCopy) { + targetWorkingCopy = await this.save(target, options); + } + + return targetWorkingCopy; + } + + // Do it + return this.doSaveAs(source, target, options); + } + + private async doSaveAs(source: URI, target: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise { + let sourceContents: VSBufferReadableStream; + + // If the source is an existing file working copy, we can directly + // use that to copy the contents to the target destination + const sourceWorkingCopy = this.get(source); + if (sourceWorkingCopy?.isResolved()) { + sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); + } + + // Otherwise we resolve the contents from the underlying file + else { + sourceContents = (await this.fileService.readFileStream(source)).value; + } + + // Save the contents through working copy to benefit from save + // participants and handling a potential already existing target + return this.doSaveAsWorkingCopy(source, sourceWorkingCopy, sourceContents, target, options); + } + + private async doSaveAsWorkingCopy(source: URI, sourceWorkingCopy: W | undefined, sourceContents: VSBufferReadableStream, target: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise { + + // Prefer an existing file working copy if it is already resolved + // for the given target resource + let targetFileExists = false; + let targetFileWorkingCopy = this.getFile(target); + if (targetFileWorkingCopy?.isResolved()) { + targetFileExists = true; + } + + // Otherwise create the target working copy empty if + // it does not exist already and resolve it from there + else { + targetFileExists = await this.fileService.exists(target); + + // Create target file adhoc if it does not exist yet + if (!targetFileExists) { + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); + } + + // At this point we need to resolve the target working copy + // and we have to do an explicit check if the source URI + // equals the target via URI identity. If they match and we + // have had an existing working copy with the source, we + // prefer that one over resolving the target. Otherwiese we + // would potentially introduce a + if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { + targetFileWorkingCopy = await this.doResolve(source); + } else { + targetFileWorkingCopy = await this.doResolve(target); + } + } + + // Confirm to overwrite if we have an untitled file working copy with associated path where + // the file actually exists on disk and we are instructed to save to that file path. + // This can happen if the file was created after the untitled file was opened. + // See https://github.com/microsoft/vscode/issues/67946 + if ( + sourceWorkingCopy instanceof UntitledFileWorkingCopy && + sourceWorkingCopy.hasAssociatedFilePath && + targetFileExists && + this.uriIdentityService.extUri.isEqual(target, toLocalResource(sourceWorkingCopy.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme)) + ) { + const overwrite = await this.confirmOverwrite(target); + if (!overwrite) { + return undefined; + } + } + + // Take over content from source to target + await targetFileWorkingCopy.model?.update(sourceContents, CancellationToken.None); + + // Save target + await targetFileWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + + // Revert the source + await sourceWorkingCopy?.revert(); + + return targetFileWorkingCopy; + } + + protected abstract doResolve(resource: URI): Promise; + + private async confirmOverwrite(resource: URI): Promise { + const confirm: IConfirmation = { + message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), + detail: localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + const result = await this.dialogService.confirm(confirm); + return result.confirmed; + } + + private async suggestSavePath(resource: URI): Promise { + + // Just take the resource as is if the file service can handle it + if (this.fileService.canHandleResource(resource)) { + return resource; + } + + const remoteAuthority = this.environmentService.remoteAuthority; + const workingCopy = this.get(resource); + + // Otherwise try to suggest a path that can be saved + let suggestedFilename: string | undefined = undefined; + if (workingCopy instanceof UntitledFileWorkingCopy) { + + // Untitled with associated file path + if (workingCopy.hasAssociatedFilePath) { + return toLocalResource(resource, remoteAuthority, this.pathService.defaultUriScheme); + } + + // Untitled without associated file path: use name + // of untitled working copy if it is a valid path name + let untitledName = workingCopy.name; + if (!isValidBasename(untitledName)) { + untitledName = basename(resource); + } + + suggestedFilename = untitledName; + } + + // Fallback to basename of resource + if (!suggestedFilename) { + suggestedFilename = basename(resource); + } + + // Try to place where last active file was if any + // Otherwise fallback to user home + return joinPath(await this.fileDialogService.defaultFilePath(), suggestedFilename); + } + //#endregion //#region Lifecycle diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index c1bc7d08104..d1222647321 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -16,13 +16,16 @@ import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { joinPath } from 'vs/base/common/resources'; import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager, IBaseFileWorkingCopySaveAsOptions } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; /** * The only one that should be dealing with `IFileWorkingCopy` and handle all @@ -101,7 +104,7 @@ export interface IFileWorkingCopyManager extend * cancellation */ saveAs(source: URI, target: URI, options?: IFileWorkingCopySaveOptions): Promise | undefined>; - saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined>; /** * Waits for the file working copy to be ready to be disposed. There may be @@ -149,15 +152,6 @@ export interface IFileWorkingCopyResolveOptions { }; } -export interface IFileWorkingCopySaveAsOptions extends IFileWorkingCopySaveOptions { - - /** - * Optional target resource to suggest to the user in case - * no taget resource is provided to save to. - */ - suggestedTarget?: URI; -} - export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events @@ -189,19 +183,23 @@ export class FileWorkingCopyManager extends Bas private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); constructor( - private readonly workingCopyTypeId: string, + workingCopyTypeId: string, private readonly modelFactory: IFileWorkingCopyModelFactory, @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, - @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, + @IFileDialogService fileDialogService: IFileDialogService, + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IDialogService dialogService: IDialogService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IPathService pathService: IPathService ) { - super(fileService, logService, workingCopyBackupService); + super(workingCopyTypeId, fileService, logService, workingCopyBackupService, fileDialogService, uriIdentityService, workingCopyFileService, dialogService, workingCopyService, environmentService, pathService); this.registerListeners(); } @@ -496,6 +494,10 @@ export class FileWorkingCopyManager extends Bas } } + protected doResolve(resource: URI): Promise> { + return this.resolve(resource); + } + private joinPendingResolve(resource: URI): Promise | undefined { const pendingWorkingCopyResolve = this.mapResourceToPendingWorkingCopyResolve.get(resource); if (pendingWorkingCopyResolve) { @@ -554,99 +556,6 @@ export class FileWorkingCopyManager extends Bas //#endregion - //#region Save As... - - async saveAs(source: URI, target?: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { - - // If not provided, ask user for target - if (!target) { - target = await this.fileDialogService.pickFileToSave(options?.suggestedTarget ?? source); - - if (!target) { - return undefined; // user canceled - } - } - - // Do it - return this.doSaveAs(source, target, options); - } - - private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { - let sourceContents: VSBufferReadableStream; - - // If the source is an existing file working copy, we can directly - // use that to copy the contents to the target destination - const sourceWorkingCopy = this.get(source); - if (sourceWorkingCopy?.isResolved()) { - sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); - } - - // Otherwise we resolve the contents from the underlying file - else { - sourceContents = (await this.fileService.readFileStream(source)).value; - } - - // Save the contents through working copy to benefit from save - // participants and handling a potential already existing target - return this.doSaveAsWorkingCopy(source, sourceContents, target, options); - } - - private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise> { - - // Prefer an existing working copy if it is already resolved - // for the given target resource - let targetExists = false; - let targetWorkingCopy = this.get(target); - if (targetWorkingCopy?.isResolved()) { - targetExists = true; - } - - // Otherwise create the target working copy empty if - // it does not exist already and resolve it from there - else { - targetExists = await this.fileService.exists(target); - - // Create target file adhoc if it does not exist yet - if (!targetExists) { - await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); - } - - // At this point we need to resolve the target working copy - // and we have to do an explicit check if the source URI - // equals the target via URI identity. If they match and we - // have had an existing working copy with the source, we - // prefer that one over resolving the target. Otherwiese we - // would potentially introduce a - if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { - targetWorkingCopy = await this.resolve(source); - } else { - targetWorkingCopy = await this.resolve(target); - } - } - - // Take over content from source to target - await targetWorkingCopy.model?.update(sourceContents, CancellationToken.None); - - // Save target - await targetWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); - - // Revert the source - await this.doRevert(source); - - return targetWorkingCopy; - } - - private async doRevert(resource: URI): Promise { - const workingCopy = this.get(resource); - if (!workingCopy) { - return undefined; - } - - return workingCopy.revert(); - } - - //#endregion - //#region Lifecycle canDispose(workingCopy: IFileWorkingCopy): true | Promise { diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 77a33c99997..21891effe76 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -61,6 +61,14 @@ export interface IResolvedUntitledFileWorkingCopy { + + /** + * A delegate to enable saving of untitled file working copies. + */ + (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; +} + export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.Untitled; @@ -91,6 +99,7 @@ export class UntitledFileWorkingCopy ex readonly hasAssociatedFilePath: boolean, private readonly initialValue: VSBufferReadableStream | undefined, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @ILogService private readonly logService: ILogService @@ -235,14 +244,7 @@ export class UntitledFileWorkingCopy ex async save(options?: ISaveOptions): Promise { this.trace('[untitled file working copy] save() - enter'); - // TODO needs to extract the code for bringing up save dialog - // or use the associated file path as target - // Also, who is disposing the untitled after save and open the - // new editor? - - await this.revert(); - - return true; + return this.saveDelegate(this, options); } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 3b2e2d4e9d4..344c038662f 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -15,6 +15,12 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService } from 'vs/platform/files/common/files'; import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; /** * The only one that should be dealing with `IUntitledFileWorkingCopy` and @@ -81,7 +87,7 @@ export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFil untitledResource: URI; } -type IInternalUntitledFileWorkingCopyOptions = IExistingUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions; +type IInternalUntitledFileWorkingCopyOptions = INewUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions & IExistingUntitledFileWorkingCopyOptions; export class UntitledFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IUntitledFileWorkingCopyManager { @@ -96,19 +102,29 @@ export class UntitledFileWorkingCopyManager, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IFileService fileService: IFileService, @ILabelService private readonly labelService: ILabelService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, + @IFileDialogService fileDialogService: IFileDialogService, + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, - @IFileService fileService: IFileService + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IDialogService dialogService: IDialogService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IPathService pathService: IPathService ) { - super(fileService, logService, workingCopyBackupService); + super(workingCopyTypeId, fileService, logService, workingCopyBackupService, fileDialogService, uriIdentityService, workingCopyFileService, dialogService, workingCopyService, environmentService, pathService); } //#region Resolve + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { const workingCopy = this.doCreateOrGet(options); await workingCopy.resolve(); @@ -116,6 +132,10 @@ export class UntitledFileWorkingCopyManager> { + return this.resolve({ untitledResource: resource }); + } + private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { const massagedOptions = this.massageOptions(options); @@ -176,7 +196,12 @@ export class UntitledFileWorkingCopyManager (async () => { + const result = await this.save(workingCopy.resource, options); + + return result ? true : false; + })() ) as unknown as IUntitledFileWorkingCopy; this.registerWorkingCopy(workingCopy); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index 47ab58bb6a7..81ef604cc9e 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -98,7 +98,7 @@ suite('FileWorkingCopy', function () { let workingCopy: FileWorkingCopy; function createWorkingCopy(uri: URI = resource) { - return new FileWorkingCopy('testWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); + return new FileWorkingCopy('testFileWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); } setup(() => { diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 4e456cc0f32..d730021d380 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -27,7 +27,23 @@ suite('FileWorkingCopyManager', () => { accessor = instantiationService.createInstance(TestServiceAccessor); const factory = new TestFileWorkingCopyModelFactory(); - manager = new FileWorkingCopyManager('testWorkingCopyType', factory, accessor.fileService, accessor.lifecycleService, accessor.labelService, instantiationService, accessor.logService, accessor.fileDialogService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService); + manager = new FileWorkingCopyManager( + 'testFileWorkingCopyType', + factory, + accessor.fileService, + accessor.lifecycleService, + accessor.labelService, + instantiationService, + accessor.logService, + accessor.fileDialogService, + accessor.workingCopyFileService, + accessor.workingCopyBackupService, + accessor.uriIdentityService, + accessor.dialogService, + accessor.workingCopyService, + accessor.environmentService, + accessor.pathService + ); }); teardown(() => { @@ -49,7 +65,7 @@ suite('FileWorkingCopyManager', () => { const workingCopy1 = await resolvePromise; assert.ok(workingCopy1); assert.ok(workingCopy1.model); - assert.strictEqual(workingCopy1.typeId, 'testWorkingCopyType'); + assert.strictEqual(workingCopy1.typeId, 'testFileWorkingCopyType'); assert.strictEqual(manager.get(resource), workingCopy1); const workingCopy2 = await manager.resolve(resource); @@ -527,7 +543,15 @@ suite('FileWorkingCopyManager', () => { // the same in that case assert.strictEqual(source.toString(), result?.resource.toString()); } else { - assert.strictEqual(target.toString(), result?.resource.toString()); + if (resolveSource || resolveTarget) { + assert.strictEqual(target.toString(), result?.resource.toString()); + } else { + if (accessor.uriIdentityService.extUri.isEqual(source, target)) { + assert.strictEqual(undefined, result); + } else { + assert.strictEqual(target.toString(), result?.resource.toString()); + } + } } if (resolveSource) { diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index d4ed35dd986..39596dcfffa 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -103,6 +103,7 @@ suite('UntitledFileWorkingCopy', () => { hasAssociatedFilePath, initialValue.length > 0 ? bufferToStream(VSBuffer.fromString(initialValue)) : undefined, factory, + async (workingCopy, options) => { return true; }, accessor.workingCopyService, accessor.workingCopyBackupService, accessor.logService diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 1dcc6163294..424426b8760 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -25,7 +25,22 @@ suite('UntitledFileWorkingCopyManager', () => { accessor = instantiationService.createInstance(TestServiceAccessor); const factory = new TestUntitledFileWorkingCopyModelFactory(); - manager = new UntitledFileWorkingCopyManager('testUntitledWorkingCopyType', factory, instantiationService, accessor.labelService, accessor.logService, accessor.workingCopyBackupService, accessor.fileService); + manager = new UntitledFileWorkingCopyManager( + 'testUntitledWorkingCopyType', + factory, + accessor.fileService, + accessor.labelService, + instantiationService, + accessor.logService, + accessor.fileDialogService, + accessor.workingCopyFileService, + accessor.workingCopyBackupService, + accessor.uriIdentityService, + accessor.dialogService, + accessor.workingCopyService, + accessor.environmentService, + accessor.pathService + ); }); teardown(() => { From a47fc96766a4a064fbac096edcc459dc9a0dec56 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 11:11:23 +0200 Subject: [PATCH 093/270] untitled file working copy - fix tests --- .../services/workingCopy/common/untitledFileWorkingCopy.ts | 2 +- .../workingCopy/test/browser/untitledFileWorkingCopy.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 21891effe76..ef9b390531f 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -241,7 +241,7 @@ export class UntitledFileWorkingCopy ex //#region Save - async save(options?: ISaveOptions): Promise { + save(options?: ISaveOptions): Promise { this.trace('[untitled file working copy] save() - enter'); return this.saveDelegate(this, options); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index 39596dcfffa..16219cd5a69 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -103,7 +103,7 @@ suite('UntitledFileWorkingCopy', () => { hasAssociatedFilePath, initialValue.length > 0 ? bufferToStream(VSBuffer.fromString(initialValue)) : undefined, factory, - async (workingCopy, options) => { return true; }, + async workingCopy => { await workingCopy.revert(); return true; }, accessor.workingCopyService, accessor.workingCopyBackupService, accessor.logService From 5835fbc9f1d63c1057ee43a1f74f64b7f4bb81c1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 14:52:15 +0200 Subject: [PATCH 094/270] untitled file working copy - some code :lipstick: --- .../common/abstractFileWorkingCopyManager.ts | 151 +++++++++--------- .../common/fileWorkingCopyManager.ts | 27 +--- .../common/untitledFileWorkingCopy.ts | 7 +- 3 files changed, 86 insertions(+), 99 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index e9021cb612d..f2f143e813e 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -39,6 +39,29 @@ export interface IBaseFileWorkingCopyManager; + saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise; + /** * Disposes all working copies of the manager and disposes the manager. This * method is different from `dispose` in that it will unregister any working @@ -119,7 +142,7 @@ export abstract class BaseFileWorkingCopyManager { const workingCopy = this.get(resource); - // Untitled + // Untitled: is always a "Save As" if (workingCopy instanceof UntitledFileWorkingCopy) { let targetUri: URI | undefined; @@ -139,7 +162,7 @@ export abstract class BaseFileWorkingCopyManager { - - // Prefer an existing file working copy if it is already resolved - // for the given target resource - let targetFileExists = false; - let targetFileWorkingCopy = this.getFile(target); - if (targetFileWorkingCopy?.isResolved()) { - targetFileExists = true; - } - - // Otherwise create the target working copy empty if - // it does not exist already and resolve it from there - else { - targetFileExists = await this.fileService.exists(target); - - // Create target file adhoc if it does not exist yet - if (!targetFileExists) { - await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); - } - - // At this point we need to resolve the target working copy - // and we have to do an explicit check if the source URI - // equals the target via URI identity. If they match and we - // have had an existing working copy with the source, we - // prefer that one over resolving the target. Otherwiese we - // would potentially introduce a - if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { - targetFileWorkingCopy = await this.doResolve(source); - } else { - targetFileWorkingCopy = await this.doResolve(target); - } - } + // Resolve target + const { targetFileExists, targetFileWorkingCopy } = await this.doResolveSaveTarget(source, target); // Confirm to overwrite if we have an untitled file working copy with associated path where // the file actually exists on disk and we are instructed to save to that file path. @@ -270,6 +256,42 @@ export abstract class BaseFileWorkingCopyManager { + + // Prefer an existing file working copy if it is already resolved + // for the given target resource + let targetFileExists = false; + let targetFileWorkingCopy = this.getFile(target); + if (targetFileWorkingCopy?.isResolved()) { + targetFileExists = true; + } + + // Otherwise create the target working copy empty if + // it does not exist already and resolve it from there + else { + targetFileExists = await this.fileService.exists(target); + + // Create target file adhoc if it does not exist yet + if (!targetFileExists) { + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); + } + + // At this point we need to resolve the target working copy + // and we have to do an explicit check if the source URI + // equals the target via URI identity. If they match and we + // have had an existing working copy with the source, we + // prefer that one over resolving the target. Otherwise we + // would potentially introduce a + if (this.uriIdentityService.extUri.isEqual(source, target) && this.has(source)) { + targetFileWorkingCopy = await this.doResolve(source); + } else { + targetFileWorkingCopy = await this.doResolve(target); + } + } + + return { targetFileExists, targetFileWorkingCopy }; + } + protected abstract doResolve(resource: URI): Promise; private async confirmOverwrite(resource: URI): Promise { @@ -286,41 +308,24 @@ export abstract class BaseFileWorkingCopyManager { - // Just take the resource as is if the file service can handle it + // 1.) Just take the resource as is if the file service can handle it if (this.fileService.canHandleResource(resource)) { return resource; } - const remoteAuthority = this.environmentService.remoteAuthority; + // 2.) Pick the associated file path for untitled working copies if any const workingCopy = this.get(resource); - - // Otherwise try to suggest a path that can be saved - let suggestedFilename: string | undefined = undefined; - if (workingCopy instanceof UntitledFileWorkingCopy) { - - // Untitled with associated file path - if (workingCopy.hasAssociatedFilePath) { - return toLocalResource(resource, remoteAuthority, this.pathService.defaultUriScheme); - } - - // Untitled without associated file path: use name - // of untitled working copy if it is a valid path name - let untitledName = workingCopy.name; - if (!isValidBasename(untitledName)) { - untitledName = basename(resource); - } - - suggestedFilename = untitledName; + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + return toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); } - // Fallback to basename of resource - if (!suggestedFilename) { - suggestedFilename = basename(resource); + // 3.) Pick the working copy name if valid joined with default path + if (workingCopy && isValidBasename(workingCopy.name)) { + return joinPath(await this.fileDialogService.defaultFilePath(), workingCopy.name); } - // Try to place where last active file was if any - // Otherwise fallback to user home - return joinPath(await this.fileDialogService.defaultFilePath(), suggestedFilename); + // 4.) Finally fallback to the name of the resource joined with default path + return joinPath(await this.fileDialogService.defaultFilePath(), basename(resource)); } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index d1222647321..80e427d8867 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -5,7 +5,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopySaveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { SaveReason } from 'vs/workbench/common/editor'; import { ResourceMap } from 'vs/base/common/map'; import { Promises, ResourceQueue } from 'vs/base/common/async'; @@ -22,7 +22,7 @@ import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/serv import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager, IBaseFileWorkingCopySaveAsOptions } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -83,29 +83,6 @@ export interface IFileWorkingCopyManager extend */ resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; - /** - * Implements "Save As" for file based working copies. The API is `URI` based - * because it works even without resolved file working copies. If a file working - * copy exists for any given `URI`, the implementation will deal with them properly - * (e.g. dirty contents of the source will be written to the target and the source - * will be reverted). - * - * Note: it is possible that the returned file working copy has a different `URI` - * than the `target` that was passed in. Based on URI identity, the file working - * copy may chose to return an existing file working copy with different casing - * to respect file systems that are case insensitive. - * - * Note: Callers must `dispose` the working copy when no longer needed. - * - * @param source the source resource to save as - * @param target the optional target resource to save to. if not defined, the user - * will be asked for input - * @returns the target working copy that was saved to or `undefined` in case of - * cancellation - */ - saveAs(source: URI, target: URI, options?: IFileWorkingCopySaveOptions): Promise | undefined>; - saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined>; - /** * Waits for the file working copy to be ready to be disposed. There may be * conditions under which the file working copy cannot be disposed, e.g. when diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index ef9b390531f..536517a7431 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -51,6 +51,11 @@ export interface IUntitledFileWorkingCopy; } export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { @@ -242,7 +247,7 @@ export class UntitledFileWorkingCopy ex //#region Save save(options?: ISaveOptions): Promise { - this.trace('[untitled file working copy] save() - enter'); + this.trace('[untitled file working copy] save()'); return this.saveDelegate(this, options); } From 37fbfa6125109387676266b2a823c12161baabf7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 15:35:56 +0200 Subject: [PATCH 095/270] untitled file working copy - set visibility --- .../workingCopy/common/abstractFileWorkingCopyManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index f2f143e813e..5ee9c15ead3 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -139,7 +139,7 @@ export abstract class BaseFileWorkingCopyManager { + protected async save(resource: URI, options?: ISaveOptions): Promise { const workingCopy = this.get(resource); // Untitled: is always a "Save As" From 0d92cb9dd88877cecb8d30150fc7c06251ceb773 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 May 2021 18:28:35 +0200 Subject: [PATCH 096/270] untitled file working copy - poperly resolve target --- .../dialogs/test/common/testDialogService.ts | 23 ++- .../common/abstractFileWorkingCopyManager.ts | 84 +++++------ .../common/fileWorkingCopyManager.ts | 27 +++- .../common/untitledFileWorkingCopyManager.ts | 23 ++- .../test/browser/fileWorkingCopy.test.ts | 8 +- .../browser/fileWorkingCopyManager.test.ts | 3 +- .../test/browser/resourceWorkingCopyTest.ts | 2 +- .../untitledFileWorkingCopyManager.test.ts | 132 +++++++++++++++++- .../test/browser/workbenchTestServices.ts | 5 +- 9 files changed, 231 insertions(+), 76 deletions(-) diff --git a/src/vs/platform/dialogs/test/common/testDialogService.ts b/src/vs/platform/dialogs/test/common/testDialogService.ts index 8d3d2465b76..1d2b2f08f71 100644 --- a/src/vs/platform/dialogs/test/common/testDialogService.ts +++ b/src/vs/platform/dialogs/test/common/testDialogService.ts @@ -10,8 +10,23 @@ export class TestDialogService implements IDialogService { declare readonly _serviceBrand: undefined; - confirm(_confirmation: IConfirmation): Promise { return Promise.resolve({ confirmed: false }); } - show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise { return Promise.resolve({ choice: 0 }); } - input(): Promise { { return Promise.resolve({ choice: 0, values: [] }); } } - about(): Promise { return Promise.resolve(); } + private confirmResult: IConfirmationResult | undefined = undefined; + setConfirmResult(result: IConfirmationResult) { + this.confirmResult = result; + } + + async confirm(confirmation: IConfirmation): Promise { + if (this.confirmResult) { + const confirmResult = this.confirmResult; + this.confirmResult = undefined; + + return confirmResult; + } + + return { confirmed: false }; + } + + async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { return { choice: 0 }; } + async input(): Promise { { return { choice: 0, values: [] }; } } + async about(): Promise { } } diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 5ee9c15ead3..337afbf2dae 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -12,7 +12,8 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; -import { FileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { FileWorkingCopy, IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { IConfirmation, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { basename, dirname, isEqual, joinPath, toLocalResource } from 'vs/base/common/resources'; @@ -59,8 +60,8 @@ export interface IBaseFileWorkingCopyManager; - saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise; + saveAs(source: URI, target: URI, options?: ISaveOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined>; /** * Disposes all working copies of the manager and disposes the manager. This @@ -90,6 +91,7 @@ export abstract class BaseFileWorkingCopyManager | undefined { const workingCopy = this.workingCopyService.get({ resource, typeId: this.workingCopyTypeId }); if (workingCopy instanceof FileWorkingCopy) { - return workingCopy as unknown as W; + return workingCopy; } return undefined; @@ -139,45 +141,16 @@ export abstract class BaseFileWorkingCopyManager { - const workingCopy = this.get(resource); - - // Untitled: is always a "Save As" - if (workingCopy instanceof UntitledFileWorkingCopy) { - let targetUri: URI | undefined; - - // Untitled with associated file path is taken as is - if (workingCopy.hasAssociatedFilePath) { - targetUri = await this.suggestSavePath(resource); - } - - // Otherwise ask user for a target path to save to - else { - targetUri = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(resource), options?.availableFileSystems); - } - - // Save as if target provided - if (targetUri) { - return this.saveAs(resource, targetUri, options); - } - } - - // File: via save method of working copy - else if (workingCopy) { - const success = await workingCopy.save(options); - if (success) { - return workingCopy; - } - } - - return undefined; - } - - async saveAs(source: URI, target?: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise { + async saveAs(source: URI, target?: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined> { // Get to target resource if (!target) { - target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + const workingCopy = this.get(source); + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + target = await this.suggestSavePath(source); + } else { + target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + } } if (!target) { @@ -187,7 +160,7 @@ export abstract class BaseFileWorkingCopyManager { + private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { + + // Save is only possible with file working copies, + // any other have to go via `saveAs` flow. + const fileWorkingCopy = this.getFile(resource); + if (fileWorkingCopy) { + const success = await fileWorkingCopy.save(options); + if (success) { + return fileWorkingCopy; + } + } + + return undefined; + } + + private async doSaveAs(source: URI, target: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined> { let sourceContents: VSBufferReadableStream; // If the source is an existing file working copy, we can directly @@ -256,7 +244,7 @@ export abstract class BaseFileWorkingCopyManager { + private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetFileWorkingCopy: IFileWorkingCopy }> { // Prefer an existing file working copy if it is already resolved // for the given target resource @@ -283,17 +271,15 @@ export abstract class BaseFileWorkingCopyManager; - private async confirmOverwrite(resource: URI): Promise { const confirm: IConfirmation = { message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 80e427d8867..ebbbb29a82e 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -129,6 +129,14 @@ export interface IFileWorkingCopyResolveOptions { }; } +export interface IFileWorkingCopyResolver { + + /** + * A delegate to resolve a file working copy. + */ + (resource: URI): Promise>; +} + export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events @@ -176,7 +184,20 @@ export class FileWorkingCopyManager extends Bas @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IPathService pathService: IPathService ) { - super(workingCopyTypeId, fileService, logService, workingCopyBackupService, fileDialogService, uriIdentityService, workingCopyFileService, dialogService, workingCopyService, environmentService, pathService); + super( + workingCopyTypeId, + resource => this.resolve(resource), + fileService, + logService, + workingCopyBackupService, + fileDialogService, + uriIdentityService, + workingCopyFileService, + dialogService, + workingCopyService, + environmentService, + pathService + ); this.registerListeners(); } @@ -471,10 +492,6 @@ export class FileWorkingCopyManager extends Bas } } - protected doResolve(resource: URI): Promise> { - return this.resolve(resource); - } - private joinPendingResolve(resource: URI): Promise | undefined { const pendingWorkingCopyResolve = this.mapResourceToPendingWorkingCopyResolve.get(resource); if (pendingWorkingCopyResolve) { diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 344c038662f..72f230c634d 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -21,6 +21,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; /** * The only one that should be dealing with `IUntitledFileWorkingCopy` and @@ -104,6 +105,7 @@ export class UntitledFileWorkingCopyManager, + fileWorkingCopyResolver: IFileWorkingCopyResolver, @IFileService fileService: IFileService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -117,7 +119,20 @@ export class UntitledFileWorkingCopyManager fileWorkingCopyResolver(resource), + fileService, + logService, + workingCopyBackupService, + fileDialogService, + uriIdentityService, + workingCopyFileService, + dialogService, + workingCopyService, + environmentService, + pathService + ); } //#region Resolve @@ -132,10 +147,6 @@ export class UntitledFileWorkingCopyManager> { - return this.resolve({ untitledResource: resource }); - } - private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { const massagedOptions = this.massageOptions(options); @@ -198,7 +209,7 @@ export class UntitledFileWorkingCopyManager (async () => { - const result = await this.save(workingCopy.resource, options); + const result = await this.saveAs(workingCopy.resource, undefined, options); return result ? true : false; })() diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index 81ef604cc9e..e01b543faa1 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -128,7 +128,7 @@ suite('FileWorkingCopy', function () { assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); let onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await onDidChangeOrphanedPromise; @@ -291,7 +291,7 @@ suite('FileWorkingCopy', function () { const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; @@ -318,7 +318,7 @@ suite('FileWorkingCopy', function () { const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; @@ -464,7 +464,7 @@ suite('FileWorkingCopy', function () { // save clears orphaned const orphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index d730021d380..cbc543d430a 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -26,10 +26,9 @@ suite('FileWorkingCopyManager', () => { instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); - const factory = new TestFileWorkingCopyModelFactory(); manager = new FileWorkingCopyManager( 'testFileWorkingCopyType', - factory, + new TestFileWorkingCopyModelFactory(), accessor.fileService, accessor.lifecycleService, accessor.labelService, diff --git a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts index 66376a44b79..e3fdc5f46b7 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopyTest.ts @@ -53,7 +53,7 @@ suite('ResourceWorkingCopy', function () { assert.strictEqual(workingCopy.isOrphaned(), false); let onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); - accessor.fileService.notExistsSet.add(resource); + accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await onDidChangeOrphanedPromise; diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 424426b8760..28119fb7798 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -8,10 +8,12 @@ import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; -import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestInMemoryFileSystemProvider, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('UntitledFileWorkingCopyManager', () => { @@ -19,15 +21,37 @@ suite('UntitledFileWorkingCopyManager', () => { let accessor: TestServiceAccessor; let manager: IUntitledFileWorkingCopyManager; + let fileManager: IFileWorkingCopyManager; setup(() => { instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); - const factory = new TestUntitledFileWorkingCopyModelFactory(); + accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); + accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); + + fileManager = new FileWorkingCopyManager( + 'testFileWorkingCopyType', + new TestFileWorkingCopyModelFactory(), + accessor.fileService, + accessor.lifecycleService, + accessor.labelService, + instantiationService, + accessor.logService, + accessor.fileDialogService, + accessor.workingCopyFileService, + accessor.workingCopyBackupService, + accessor.uriIdentityService, + accessor.dialogService, + accessor.workingCopyService, + accessor.environmentService, + accessor.pathService + ); + manager = new UntitledFileWorkingCopyManager( 'testUntitledWorkingCopyType', - factory, + new TestUntitledFileWorkingCopyModelFactory(), + resource => fileManager.resolve(resource), accessor.fileService, accessor.labelService, instantiationService, @@ -141,6 +165,108 @@ suite('UntitledFileWorkingCopyManager', () => { assert.strictEqual(workingCopy.hasAssociatedFilePath, true); assert.strictEqual(workingCopy.resource.path, '/some/associated.txt'); + + workingCopy.dispose(); + }); + + test('save - without associated resource', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save'); + + accessor.fileDialogService.setPickFileToSave(URI.file('simple/file.txt')); + + const result = await workingCopy.save(); + assert.ok(result); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('save - with associated resource', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save with associated resource'); + + accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }), true); + + const result = await workingCopy.save(); + assert.ok(result); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('save - with associated resource (asks to overwrite)', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save with associated resource'); + + let result = await workingCopy.save(); + assert.ok(!result); // not confirmed + + assert.strictEqual(manager.get(workingCopy.resource), workingCopy); + + accessor.dialogService.setConfirmResult({ confirmed: true }); + + result = await workingCopy.save(); + assert.ok(result); // confirmed + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - without associated resource', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - with associated resource', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save As with associated resource'); + + const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); + + accessor.fileService.notExistsSet.set(target, true); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - target exists and is resolved', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + const targetFileWorkingCopy = await fileManager.resolve(target); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result, targetFileWorkingCopy); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.get(workingCopy.resource), undefined); + + workingCopy.dispose(); }); test('destroy', async () => { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 31a0a9caab2..6ea06def96d 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -138,6 +138,7 @@ import { IElevatedFileService } from 'vs/workbench/services/files/common/elevate import { BrowserElevatedFileService } from 'vs/workbench/services/files/browser/elevatedFileService'; import { IDiffComputationResult, IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/modes'; +import { ResourceMap } from 'vs/base/common/map'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined); @@ -261,7 +262,7 @@ export class TestServiceAccessor { @IModelService public modelService: ModelServiceImpl, @IFileService public fileService: TestFileService, @IFileDialogService public fileDialogService: TestFileDialogService, - @IDialogService public dialogService: IDialogService, + @IDialogService public dialogService: TestDialogService, @IWorkingCopyService public workingCopyService: IWorkingCopyService, @IEditorService public editorService: TestEditorService, @IWorkbenchEnvironmentService public environmentService: IWorkbenchEnvironmentService, @@ -870,7 +871,7 @@ export class TestFileService implements IFileService { return stats.map(stat => ({ stat, success: true })); } - readonly notExistsSet = new Set(); + readonly notExistsSet = new ResourceMap(); async exists(_resource: URI): Promise { return !this.notExistsSet.has(_resource); } From bc37b284431575c5b74755c19e7aca4aa11441d9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 07:35:54 +0200 Subject: [PATCH 097/270] untitled file working copy - shared dispose handling --- .../common/abstractFileWorkingCopy.ts | 2 +- .../common/abstractFileWorkingCopyManager.ts | 24 ++++++++++ .../common/fileWorkingCopyManager.ts | 30 +------------ .../common/untitledFileWorkingCopyManager.ts | 45 +++++++++++++------ 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts index aa49c394e09..3568f1ca66b 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts @@ -24,7 +24,7 @@ export interface IBaseFileWorkingCopyModelFactory> extends Disposable implements IBaseFileWorkingCopyManager { private readonly mapResourceToWorkingCopy = new ResourceMap(); + private readonly mapResourceToDisposeListener = new ResourceMap(); constructor( protected readonly workingCopyTypeId: string, @@ -111,10 +112,29 @@ export abstract class BaseFileWorkingCopyManager this.remove(resource))); } protected remove(resource: URI): void { + + // Dispose any existing listener + const disposeListener = this.mapResourceToDisposeListener.get(resource); + if (disposeListener) { + dispose(disposeListener); + this.mapResourceToDisposeListener.delete(resource); + } + + // Remove from our working copy map this.mapResourceToWorkingCopy.delete(resource); } @@ -330,6 +350,10 @@ export abstract class BaseFileWorkingCopyManager { diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index ebbbb29a82e..309f5e80655 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -162,7 +162,6 @@ export class FileWorkingCopyManager extends Bas //#endregion private readonly mapResourceToWorkingCopyListeners = new ResourceMap(); - private readonly mapResourceToDisposeListener = new ResourceMap(); private readonly mapResourceToPendingWorkingCopyResolve = new ResourceMap>(); private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); @@ -515,32 +514,10 @@ export class FileWorkingCopyManager extends Bas this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); } - protected override add(resource: URI, workingCopy: IFileWorkingCopy): void { - const knownWorkingCopy = this.get(resource); - if (knownWorkingCopy === workingCopy) { - return; // already cached - } - - // Dispose any previously stored dispose listener for this resource - const disposeListener = this.mapResourceToDisposeListener.get(resource); - if (disposeListener) { - disposeListener.dispose(); - } - - // Store in cache but remove when working copy gets disposed - super.add(resource, workingCopy); - this.mapResourceToDisposeListener.set(resource, workingCopy.onWillDispose(() => this.remove(resource))); - } - protected override remove(resource: URI): void { super.remove(resource); - const disposeListener = this.mapResourceToDisposeListener.get(resource); - if (disposeListener) { - dispose(disposeListener); - this.mapResourceToDisposeListener.delete(resource); - } - + // Dispose any exsting working copy listeners const workingCopyListener = this.mapResourceToWorkingCopyListeners.get(resource); if (workingCopyListener) { dispose(workingCopyListener); @@ -591,12 +568,9 @@ export class FileWorkingCopyManager extends Bas override dispose(): void { super.dispose(); + // Clear pending working copy resolves this.mapResourceToPendingWorkingCopyResolve.clear(); - // Dispose the dispose listeners - dispose(this.mapResourceToDisposeListener.values()); - this.mapResourceToDisposeListener.clear(); - // Dispose the working copy change listeners dispose(this.mapResourceToWorkingCopyListeners.values()); this.mapResourceToWorkingCopyListeners.clear(); diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 72f230c634d..28ee5986867 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { Event, Emitter } from 'vs/base/common/event'; @@ -22,6 +22,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { ResourceMap } from 'vs/base/common/map'; /** * The only one that should be dealing with `IUntitledFileWorkingCopy` and @@ -102,6 +103,8 @@ export class UntitledFileWorkingCopyManager(); + constructor( workingCopyTypeId: string, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, @@ -223,19 +226,12 @@ export class UntitledFileWorkingCopyManager): void { // Install working copy listeners - const listeners = new DisposableStore(); - listeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); - listeners.add(workingCopy.onWillDispose(() => this._onWillDispose.fire(workingCopy))); + const workingCopyListeners = new DisposableStore(); + workingCopyListeners.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); + workingCopyListeners.add(workingCopy.onWillDispose(() => this._onWillDispose.fire(workingCopy))); - // Remove from cache on dispose - Event.once(workingCopy.onWillDispose)(() => { - - // Registry - this.remove(workingCopy.resource); - - // Listeners - listeners.dispose(); - }); + // Keep for disposal + this.mapResourceToWorkingCopyListeners.set(workingCopy.resource, workingCopyListeners); // Add to cache this.add(workingCopy.resource, workingCopy); @@ -247,5 +243,28 @@ export class UntitledFileWorkingCopyManager Date: Wed, 19 May 2021 09:00:39 +0200 Subject: [PATCH 098/270] untitled file working copy - add new manager that unifies file and untitled working copies --- .../common/abstractFileWorkingCopyManager.ts | 244 +----------- .../common/fileWorkingCopyManager.ts | 144 +++++-- .../common/fileWorkingCopyManager2.ts | 353 ++++++++++++++++++ .../common/untitledFileWorkingCopy.ts | 6 +- .../common/untitledFileWorkingCopyManager.ts | 43 +-- .../browser/fileWorkingCopyManager.test.ts | 16 +- .../untitledFileWorkingCopyManager.test.ts | 151 ++------ 7 files changed, 512 insertions(+), 445 deletions(-) create mode 100644 src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 19ffab8a89b..0659e102304 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { Promises } from 'vs/base/common/async'; @@ -12,20 +11,6 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; -import { FileWorkingCopy, IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; -import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; -import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; -import { IConfirmation, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { basename, dirname, isEqual, joinPath, toLocalResource } from 'vs/base/common/resources'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { isValidBasename } from 'vs/base/common/extpath'; -import { ISaveOptions } from 'vs/workbench/common/editor'; export interface IBaseFileWorkingCopyManager> extends IDisposable { @@ -40,29 +25,6 @@ export interface IBaseFileWorkingCopyManager | undefined>; - saveAs(source: URI, target: undefined, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined>; - /** * Disposes all working copies of the manager and disposes the manager. This * method is different from `dispose` in that it will unregister any working @@ -76,33 +38,15 @@ export interface IBaseFileWorkingCopyManager; } -export interface IBaseFileWorkingCopySaveAsOptions extends ISaveOptions { - - /** - * Optional target resource to suggest to the user in case - * no taget resource is provided to save to. - */ - suggestedTarget?: URI; -} - export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { private readonly mapResourceToWorkingCopy = new ResourceMap(); private readonly mapResourceToDisposeListener = new ResourceMap(); constructor( - protected readonly workingCopyTypeId: string, - private readonly fileWorkingCopyResolver: IFileWorkingCopyResolver, @IFileService protected readonly fileService: IFileService, @ILogService protected readonly logService: ILogService, - @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, - @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, - @IWorkingCopyFileService protected readonly workingCopyFileService: IWorkingCopyFileService, - @IDialogService private readonly dialogService: IDialogService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IPathService private readonly pathService: IPathService + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService ) { super(); } @@ -148,192 +92,6 @@ export abstract class BaseFileWorkingCopyManager | undefined { - const workingCopy = this.workingCopyService.get({ resource, typeId: this.workingCopyTypeId }); - if (workingCopy instanceof FileWorkingCopy) { - return workingCopy; - } - - return undefined; - } - - //#endregion - - //#region Save - - async saveAs(source: URI, target?: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined> { - - // Get to target resource - if (!target) { - const workingCopy = this.get(source); - if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { - target = await this.suggestSavePath(source); - } else { - target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); - } - } - - if (!target) { - return; // user canceled - } - - // Just save if target is same as working copies own resource - // and we are not saving an untitled file working copy - if (this.fileService.canHandleResource(source) && isEqual(source, target)) { - return this.doSave(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); - } - - // If the target is different but of same identity, we - // move the source to the target, knowing that the - // underlying file system cannot have both and then save. - // However, this will only work if the source exists - // and is not orphaned, so we need to check that too. - if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) { - - // Move via working copy file service to enable participants - await this.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); - - // At this point we don't know whether we have a - // working copy for the source or the target URI so we - // simply try to save with both resources. - return (await this.doSave(source, options)) ?? (await this.doSave(target, options)); - } - - // Perform normal "Save As" - return this.doSaveAs(source, target, options); - } - - private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { - - // Save is only possible with file working copies, - // any other have to go via `saveAs` flow. - const fileWorkingCopy = this.getFile(resource); - if (fileWorkingCopy) { - const success = await fileWorkingCopy.save(options); - if (success) { - return fileWorkingCopy; - } - } - - return undefined; - } - - private async doSaveAs(source: URI, target: URI, options?: IBaseFileWorkingCopySaveAsOptions): Promise | undefined> { - let sourceContents: VSBufferReadableStream; - - // If the source is an existing file working copy, we can directly - // use that to copy the contents to the target destination - const sourceWorkingCopy = this.get(source); - if (sourceWorkingCopy?.isResolved()) { - sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); - } - - // Otherwise we resolve the contents from the underlying file - else { - sourceContents = (await this.fileService.readFileStream(source)).value; - } - - // Resolve target - const { targetFileExists, targetFileWorkingCopy } = await this.doResolveSaveTarget(source, target); - - // Confirm to overwrite if we have an untitled file working copy with associated path where - // the file actually exists on disk and we are instructed to save to that file path. - // This can happen if the file was created after the untitled file was opened. - // See https://github.com/microsoft/vscode/issues/67946 - if ( - sourceWorkingCopy instanceof UntitledFileWorkingCopy && - sourceWorkingCopy.hasAssociatedFilePath && - targetFileExists && - this.uriIdentityService.extUri.isEqual(target, toLocalResource(sourceWorkingCopy.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme)) - ) { - const overwrite = await this.confirmOverwrite(target); - if (!overwrite) { - return undefined; - } - } - - // Take over content from source to target - await targetFileWorkingCopy.model?.update(sourceContents, CancellationToken.None); - - // Save target - await targetFileWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); - - // Revert the source - await sourceWorkingCopy?.revert(); - - return targetFileWorkingCopy; - } - - private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetFileWorkingCopy: IFileWorkingCopy }> { - - // Prefer an existing file working copy if it is already resolved - // for the given target resource - let targetFileExists = false; - let targetFileWorkingCopy = this.getFile(target); - if (targetFileWorkingCopy?.isResolved()) { - targetFileExists = true; - } - - // Otherwise create the target working copy empty if - // it does not exist already and resolve it from there - else { - targetFileExists = await this.fileService.exists(target); - - // Create target file adhoc if it does not exist yet - if (!targetFileExists) { - await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); - } - - // At this point we need to resolve the target working copy - // and we have to do an explicit check if the source URI - // equals the target via URI identity. If they match and we - // have had an existing working copy with the source, we - // prefer that one over resolving the target. Otherwise we - // would potentially introduce a - if (this.uriIdentityService.extUri.isEqual(source, target) && this.has(source)) { - targetFileWorkingCopy = await this.fileWorkingCopyResolver(source); - } else { - targetFileWorkingCopy = await this.fileWorkingCopyResolver(target); - } - } - - return { targetFileExists, targetFileWorkingCopy }; - } - - private async confirmOverwrite(resource: URI): Promise { - const confirm: IConfirmation = { - message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), - detail: localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))), - primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), - type: 'warning' - }; - - const result = await this.dialogService.confirm(confirm); - return result.confirmed; - } - - private async suggestSavePath(resource: URI): Promise { - - // 1.) Just take the resource as is if the file service can handle it - if (this.fileService.canHandleResource(resource)) { - return resource; - } - - // 2.) Pick the associated file path for untitled working copies if any - const workingCopy = this.get(resource); - if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { - return toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); - } - - // 3.) Pick the working copy name if valid joined with default path - if (workingCopy && isValidBasename(workingCopy.name)) { - return joinPath(await this.fileDialogService.defaultFilePath(), workingCopy.name); - } - - // 4.) Finally fallback to the name of the resource joined with default path - return joinPath(await this.fileDialogService.defaultFilePath(), basename(resource)); - } - //#endregion //#region Lifecycle diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 309f5e80655..f1c7354fa8b 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -16,16 +16,13 @@ import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { joinPath } from 'vs/base/common/resources'; import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; /** * The only one that should be dealing with `IFileWorkingCopy` and handle all @@ -89,6 +86,13 @@ export interface IFileWorkingCopyManager extend * it is dirty. Once the promise is settled, it is safe to dispose. */ canDispose(workingCopy: IFileWorkingCopy): true | Promise; + + + /** + * @deprecated + */ + saveAs(source: URI, target: URI): Promise | undefined>; + saveAs(source: URI, target: undefined): Promise | undefined>; } export interface IFileWorkingCopySaveEvent { @@ -129,14 +133,6 @@ export interface IFileWorkingCopyResolveOptions { }; } -export interface IFileWorkingCopyResolver { - - /** - * A delegate to resolve a file working copy. - */ - (resource: URI): Promise>; -} - export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events @@ -167,36 +163,19 @@ export class FileWorkingCopyManager extends Bas private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); constructor( - workingCopyTypeId: string, + private readonly workingCopyTypeId: string, private readonly modelFactory: IFileWorkingCopyModelFactory, @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, - @IFileDialogService fileDialogService: IFileDialogService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, - @IUriIdentityService uriIdentityService: IUriIdentityService, - @IDialogService dialogService: IDialogService, - @IWorkingCopyService workingCopyService: IWorkingCopyService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IPathService pathService: IPathService + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IFileDialogService private readonly fileDialogService: IFileDialogService ) { - super( - workingCopyTypeId, - resource => this.resolve(resource), - fileService, - logService, - workingCopyBackupService, - fileDialogService, - uriIdentityService, - workingCopyFileService, - dialogService, - workingCopyService, - environmentService, - pathService - ); + super(fileService, logService, workingCopyBackupService); this.registerListeners(); } @@ -577,4 +556,101 @@ export class FileWorkingCopyManager extends Bas } //#endregion + + + //#region Save As... + + /** + * @deprecated TODO@bpasero remove + */ + async saveAs(source: URI, target?: URI): Promise | undefined> { + + // If not provided, ask user for target + if (!target) { + target = await this.fileDialogService.pickFileToSave(source); + + if (!target) { + return undefined; // user canceled + } + } + + // Do it + return this.doSaveAs(source, target); + } + + private async doSaveAs(source: URI, target: URI): Promise | undefined> { + let sourceContents: VSBufferReadableStream; + + // If the source is an existing file working copy, we can directly + // use that to copy the contents to the target destination + const sourceWorkingCopy = this.get(source); + if (sourceWorkingCopy?.isResolved()) { + sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); + } + + // Otherwise we resolve the contents from the underlying file + else { + sourceContents = (await this.fileService.readFileStream(source)).value; + } + + // Save the contents through working copy to benefit from save + // participants and handling a potential already existing target + return this.doSaveAsWorkingCopy(source, sourceContents, target); + } + + private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI): Promise> { + + // Prefer an existing working copy if it is already resolved + // for the given target resource + let targetExists = false; + let targetWorkingCopy = this.get(target); + if (targetWorkingCopy?.isResolved()) { + targetExists = true; + } + + // Otherwise create the target working copy empty if + // it does not exist already and resolve it from there + else { + targetExists = await this.fileService.exists(target); + + // Create target file adhoc if it does not exist yet + if (!targetExists) { + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); + } + + // At this point we need to resolve the target working copy + // and we have to do an explicit check if the source URI + // equals the target via URI identity. If they match and we + // have had an existing working copy with the source, we + // prefer that one over resolving the target. Otherwiese we + // would potentially introduce a + if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { + targetWorkingCopy = await this.resolve(source); + } else { + targetWorkingCopy = await this.resolve(target); + } + } + + // Take over content from source to target + await targetWorkingCopy.model?.update(sourceContents, CancellationToken.None); + + // Save target + await targetWorkingCopy.save({ force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + + // Revert the source + await this.doRevert(source); + + return targetWorkingCopy; + } + + private async doRevert(resource: URI): Promise { + const workingCopy = this.get(resource); + if (!workingCopy) { + return undefined; + } + + return workingCopy.revert(); + } + + //#endregion } diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts new file mode 100644 index 00000000000..b6c1944af3a --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts @@ -0,0 +1,353 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { Promises } from 'vs/base/common/async'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { toLocalResource, joinPath, isEqual, basename, dirname } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { IFileDialogService, IDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ISaveOptions } from 'vs/workbench/common/editor'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { IExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyWithAssociatedResourceOptions, IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { isValidBasename } from 'vs/base/common/extpath'; + +export interface IFileWorkingCopyManager2 extends IDisposable { + + /** + * Provides access to the manager for titled file working copies. + */ + readonly files: IFileWorkingCopyManager; + + /** + * Provides access to the manager for untitled file working copies. + */ + readonly untitled: IUntitledFileWorkingCopyManager; + + /** + * Resolves an untitled file working copy from the provided options. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Resolves an untitled file working copy from the provided options + * unless an existing working copy already exists with that resource. + */ + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + + /** + * Allows to resolve a file working copy. If the manager already knows + * about a file working copy with the same `URI`, it will return that + * existing file working copy. There will never be more than one + * file working copy per `URI` until the file working copy is disposed. + * + * Use the `IFileWorkingCopyResolveOptions.reload` option to control the + * behaviour for when a file working copy was previously already resolved + * with regards to resolving it again from the underlying file resource + * or not. + * + * Note: Callers must `dispose` the working copy when no longer needed. + * + * @param resource used as unique identifier of the file working copy in + * case one is already known for this `URI`. + * @param options + */ + resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; + + /** + * Implements "Save As" for file based working copies. The API is `URI` based + * because it works even without resolved file working copies. If a file working + * copy exists for any given `URI`, the implementation will deal with them properly + * (e.g. dirty contents of the source will be written to the target and the source + * will be reverted). + * + * Note: it is possible that the returned file working copy has a different `URI` + * than the `target` that was passed in. Based on URI identity, the file working + * copy may chose to return an existing file working copy with different casing + * to respect file systems that are case insensitive. + * + * Note: Callers must `dispose` the working copy when no longer needed. + * + * @param source the source resource to save as + * @param target the optional target resource to save to. if not defined, the user + * will be asked for input + * @returns the target working copy that was saved to or `undefined` in case of + * cancellation + */ + saveAs(source: URI, target: URI, options?: ISaveOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; + + /** + * Disposes all working copies of the manager and disposes the manager. This + * method is different from `dispose` in that it will unregister any working + * copy from the `IWorkingCopyService`. Since this impact things like backups, + * the method is `async` because it needs to trigger `save` for any dirty + * working copy to preserve the data. + * + * Callers should make sure to e.g. close any editors associated with the + * working copy. + */ + destroy(): Promise; +} + +export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { + + /** + * Optional target resource to suggest to the user in case + * no taget resource is provided to save to. + */ + suggestedTarget?: URI; +} + +export class FileWorkingCopyManager2 extends Disposable implements IFileWorkingCopyManager2 { + + readonly files: IFileWorkingCopyManager; + readonly untitled: IUntitledFileWorkingCopyManager; + + constructor( + private readonly workingCopyTypeId: string, + private readonly fileModelFactory: IFileWorkingCopyModelFactory, + private readonly untitledFileModelFactory: IUntitledFileWorkingCopyModelFactory, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IDialogService private readonly dialogService: IDialogService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IPathService private readonly pathService: IPathService + ) { + super(); + + // File manager + this.files = this._register(this.instantiationService.createInstance(FileWorkingCopyManager, this.workingCopyTypeId, this.fileModelFactory)) as unknown as IFileWorkingCopyManager; + + // Untitled manager + this.untitled = this._register(this.instantiationService.createInstance(UntitledFileWorkingCopyManager, this.workingCopyTypeId, this.untitledFileModelFactory, async (workingCopy, options) => { + const result = await this.saveAs(workingCopy.resource, undefined, options); + + return result ? true : false; + })) as unknown as IUntitledFileWorkingCopyManager; + } + + //#region resolve + + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; + resolve(arg1?: URI | INewUntitledFileWorkingCopyOptions | INewUntitledFileWorkingCopyWithAssociatedResourceOptions | IExistingUntitledFileWorkingCopyOptions, arg2?: IFileWorkingCopyResolveOptions): Promise | IFileWorkingCopy> { + if (URI.isUri(arg1)) { + return this.files.resolve(arg1, arg2); + } + + return this.untitled.resolve(arg1); + } + + //#endregion + + //#region Save + + async saveAs(source: URI, target?: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { + + // Get to target resource + if (!target) { + const workingCopy = this.get(source); + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + target = await this.suggestSavePath(source); + } else { + target = await this.fileDialogService.pickFileToSave(await this.suggestSavePath(options?.suggestedTarget ?? source), options?.availableFileSystems); + } + } + + if (!target) { + return; // user canceled + } + + // Just save if target is same as working copies own resource + // and we are not saving an untitled file working copy + if (this.fileService.canHandleResource(source) && isEqual(source, target)) { + return this.doSave(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + } + + // If the target is different but of same identity, we + // move the source to the target, knowing that the + // underlying file system cannot have both and then save. + // However, this will only work if the source exists + // and is not orphaned, so we need to check that too. + if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) { + + // Move via working copy file service to enable participants + await this.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); + + // At this point we don't know whether we have a + // working copy for the source or the target URI so we + // simply try to save with both resources. + return (await this.doSave(source, options)) ?? (await this.doSave(target, options)); + } + + // Perform normal "Save As" + return this.doSaveAs(source, target, options); + } + + private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { + + // Save is only possible with file working copies, + // any other have to go via `saveAs` flow. + const fileWorkingCopy = this.files.get(resource); + if (fileWorkingCopy) { + const success = await fileWorkingCopy.save(options); + if (success) { + return fileWorkingCopy; + } + } + + return undefined; + } + + private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { + let sourceContents: VSBufferReadableStream; + + // If the source is an existing file working copy, we can directly + // use that to copy the contents to the target destination + const sourceWorkingCopy = this.get(source); + if (sourceWorkingCopy?.isResolved()) { + sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); + } + + // Otherwise we resolve the contents from the underlying file + else { + sourceContents = (await this.fileService.readFileStream(source)).value; + } + + // Resolve target + const { targetFileExists, targetFileWorkingCopy } = await this.doResolveSaveTarget(source, target); + + // Confirm to overwrite if we have an untitled file working copy with associated path where + // the file actually exists on disk and we are instructed to save to that file path. + // This can happen if the file was created after the untitled file was opened. + // See https://github.com/microsoft/vscode/issues/67946 + if ( + sourceWorkingCopy instanceof UntitledFileWorkingCopy && + sourceWorkingCopy.hasAssociatedFilePath && + targetFileExists && + this.uriIdentityService.extUri.isEqual(target, toLocalResource(sourceWorkingCopy.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme)) + ) { + const overwrite = await this.confirmOverwrite(target); + if (!overwrite) { + return undefined; + } + } + + // Take over content from source to target + await targetFileWorkingCopy.model?.update(sourceContents, CancellationToken.None); + + // Save target + await targetFileWorkingCopy.save({ ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); + + // Revert the source + await sourceWorkingCopy?.revert(); + + return targetFileWorkingCopy; + } + + private get(resource: URI): IUntitledFileWorkingCopy | IFileWorkingCopy | undefined { + return this.files.get(resource) ?? this.untitled.get(resource); + } + + private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetFileWorkingCopy: IFileWorkingCopy }> { + + // Prefer an existing file working copy if it is already resolved + // for the given target resource + let targetFileExists = false; + let targetFileWorkingCopy = this.files.get(target); + if (targetFileWorkingCopy?.isResolved()) { + targetFileExists = true; + } + + // Otherwise create the target working copy empty if + // it does not exist already and resolve it from there + else { + targetFileExists = await this.fileService.exists(target); + + // Create target file adhoc if it does not exist yet + if (!targetFileExists) { + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); + } + + // At this point we need to resolve the target working copy + // and we have to do an explicit check if the source URI + // equals the target via URI identity. If they match and we + // have had an existing working copy with the source, we + // prefer that one over resolving the target. Otherwise we + // would potentially introduce a + if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { + targetFileWorkingCopy = await this.files.resolve(source); + } else { + targetFileWorkingCopy = await this.files.resolve(target); + } + } + + return { targetFileExists, targetFileWorkingCopy }; + } + + private async confirmOverwrite(resource: URI): Promise { + const confirm: IConfirmation = { + message: localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)), + detail: localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))), + primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"), + type: 'warning' + }; + + const result = await this.dialogService.confirm(confirm); + return result.confirmed; + } + + private async suggestSavePath(resource: URI): Promise { + + // 1.) Just take the resource as is if the file service can handle it + if (this.fileService.canHandleResource(resource)) { + return resource; + } + + // 2.) Pick the associated file path for untitled working copies if any + const workingCopy = this.get(resource); + if (workingCopy instanceof UntitledFileWorkingCopy && workingCopy.hasAssociatedFilePath) { + return toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); + } + + // 3.) Pick the working copy name if valid joined with default path + if (workingCopy && isValidBasename(workingCopy.name)) { + return joinPath(await this.fileDialogService.defaultFilePath(), workingCopy.name); + } + + // 4.) Finally fallback to the name of the resource joined with default path + return joinPath(await this.fileDialogService.defaultFilePath(), basename(resource)); + } + + //#endregion + + //#region Lifecycle + + async destroy(): Promise { + await Promises.settled([ + this.files.destroy(), + this.untitled.destroy() + ]); + } + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 536517a7431..90d24089bbb 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -66,12 +66,12 @@ export interface IResolvedUntitledFileWorkingCopy { +export interface IUntitledFileWorkingCopySaveDelegate { /** * A delegate to enable saving of untitled file working copies. */ - (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; + (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; } export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { @@ -104,7 +104,7 @@ export class UntitledFileWorkingCopy ex readonly hasAssociatedFilePath: boolean, private readonly initialValue: VSBufferReadableStream | undefined, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, - private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @ILogService private readonly logService: ILogService diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 28ee5986867..09562661f3d 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -6,7 +6,7 @@ import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, IUntitledFileWorkingCopySaveDelegate, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { Event, Emitter } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,13 +15,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService } from 'vs/platform/files/common/files'; import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; -import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IFileWorkingCopyResolver } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { ResourceMap } from 'vs/base/common/map'; /** @@ -106,36 +99,16 @@ export class UntitledFileWorkingCopyManager(); constructor( - workingCopyTypeId: string, + private readonly workingCopyTypeId: string, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, - fileWorkingCopyResolver: IFileWorkingCopyResolver, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IFileService fileService: IFileService, @ILabelService private readonly labelService: ILabelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, - @IFileDialogService fileDialogService: IFileDialogService, - @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, - @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, - @IUriIdentityService uriIdentityService: IUriIdentityService, - @IDialogService dialogService: IDialogService, - @IWorkingCopyService workingCopyService: IWorkingCopyService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IPathService pathService: IPathService + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService ) { - super( - workingCopyTypeId, - resource => fileWorkingCopyResolver(resource), - fileService, - logService, - workingCopyBackupService, - fileDialogService, - uriIdentityService, - workingCopyFileService, - dialogService, - workingCopyService, - environmentService, - pathService - ); + super(fileService, logService, workingCopyBackupService); } //#region Resolve @@ -211,11 +184,7 @@ export class UntitledFileWorkingCopyManager (async () => { - const result = await this.saveAs(workingCopy.resource, undefined, options); - - return result ? true : false; - })() + this.saveDelegate ) as unknown as IUntitledFileWorkingCopy; this.registerWorkingCopy(workingCopy); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index cbc543d430a..9905e3ac630 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -34,14 +34,10 @@ suite('FileWorkingCopyManager', () => { accessor.labelService, instantiationService, accessor.logService, - accessor.fileDialogService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, - accessor.dialogService, - accessor.workingCopyService, - accessor.environmentService, - accessor.pathService + accessor.fileDialogService ); }); @@ -542,15 +538,7 @@ suite('FileWorkingCopyManager', () => { // the same in that case assert.strictEqual(source.toString(), result?.resource.toString()); } else { - if (resolveSource || resolveTarget) { - assert.strictEqual(target.toString(), result?.resource.toString()); - } else { - if (accessor.uriIdentityService.extUri.isEqual(source, target)) { - assert.strictEqual(undefined, result); - } else { - assert.strictEqual(target.toString(), result?.resource.toString()); - } - } + assert.strictEqual(target.toString(), result?.resource.toString()); } if (resolveSource) { diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 28119fb7798..8a7b56c90d3 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -8,8 +8,7 @@ import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; -import { IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; +import { FileWorkingCopyManager2, IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; @@ -20,8 +19,7 @@ suite('UntitledFileWorkingCopyManager', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let manager: IUntitledFileWorkingCopyManager; - let fileManager: IFileWorkingCopyManager; + let manager: IFileWorkingCopyManager2; setup(() => { instantiationService = workbenchInstantiationService(); @@ -30,38 +28,16 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); - fileManager = new FileWorkingCopyManager( - 'testFileWorkingCopyType', + manager = new FileWorkingCopyManager2( + 'testFileWorkingCopyTypeUntitled', new TestFileWorkingCopyModelFactory(), - accessor.fileService, - accessor.lifecycleService, - accessor.labelService, - instantiationService, - accessor.logService, - accessor.fileDialogService, - accessor.workingCopyFileService, - accessor.workingCopyBackupService, - accessor.uriIdentityService, - accessor.dialogService, - accessor.workingCopyService, - accessor.environmentService, - accessor.pathService - ); - - manager = new UntitledFileWorkingCopyManager( - 'testUntitledWorkingCopyType', new TestUntitledFileWorkingCopyModelFactory(), - resource => fileManager.resolve(resource), accessor.fileService, - accessor.labelService, instantiationService, - accessor.logService, accessor.fileDialogService, accessor.workingCopyFileService, - accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.dialogService, - accessor.workingCopyService, accessor.environmentService, accessor.pathService ); @@ -73,29 +49,29 @@ suite('UntitledFileWorkingCopyManager', () => { test('basics', async () => { let disposeCounter = 0; - manager.onWillDispose(e => { + manager.untitled.onWillDispose(e => { disposeCounter++; }); let dirtyCounter = 0; - manager.onDidChangeDirty(e => { + manager.untitled.onDidChangeDirty(e => { dirtyCounter++; }); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); - assert.strictEqual(manager.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); - assert.strictEqual(manager.get(URI.file('/some/invalidPath')), undefined); - assert.strictEqual(manager.get(URI.file('/some/invalidPath').with({ scheme: Schemas.untitled })), undefined); + assert.strictEqual(manager.untitled.get(URI.file('/some/invalidPath')), undefined); + assert.strictEqual(manager.untitled.get(URI.file('/some/invalidPath').with({ scheme: Schemas.untitled })), undefined); - const workingCopy1 = await manager.resolve(); - const workingCopy2 = await manager.resolve(); + const workingCopy1 = await manager.untitled.resolve(); + const workingCopy2 = await manager.untitled.resolve(); - assert.strictEqual(manager.get(workingCopy1.resource), workingCopy1); - assert.strictEqual(manager.get(workingCopy2.resource), workingCopy2); + assert.strictEqual(manager.untitled.get(workingCopy1.resource), workingCopy1); + assert.strictEqual(manager.untitled.get(workingCopy2.resource), workingCopy2); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); - assert.strictEqual(manager.workingCopies.length, 2); + assert.strictEqual(manager.untitled.workingCopies.length, 2); assert.notStrictEqual(workingCopy1.resource.toString(), workingCopy2.resource.toString()); @@ -120,24 +96,24 @@ suite('UntitledFileWorkingCopyManager', () => { workingCopy1.dispose(); - assert.strictEqual(manager.workingCopies.length, 1); - assert.strictEqual(manager.get(workingCopy1.resource), undefined); + assert.strictEqual(manager.untitled.workingCopies.length, 1); + assert.strictEqual(manager.untitled.get(workingCopy1.resource), undefined); workingCopy2.dispose(); - assert.strictEqual(manager.workingCopies.length, 0); - assert.strictEqual(manager.get(workingCopy2.resource), undefined); + assert.strictEqual(manager.untitled.workingCopies.length, 0); + assert.strictEqual(manager.untitled.get(workingCopy2.resource), undefined); assert.strictEqual(disposeCounter, 2); }); test('resolve - with initial value', async () => { let dirtyCounter = 0; - manager.onDidChangeDirty(e => { + manager.untitled.onDidChangeDirty(e => { dirtyCounter++; }); - const workingCopy = await manager.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello World')) }); + const workingCopy = await manager.untitled.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello World')) }); assert.strictEqual(workingCopy.isDirty(), true); assert.strictEqual(dirtyCounter, 1); @@ -147,12 +123,12 @@ suite('UntitledFileWorkingCopyManager', () => { }); test('resolve - existing', async () => { - const workingCopy1 = await manager.resolve(); + const workingCopy1 = await manager.untitled.resolve(); - const workingCopy2 = await manager.resolve({ untitledResource: workingCopy1.resource }); + const workingCopy2 = await manager.untitled.resolve({ untitledResource: workingCopy1.resource }); assert.strictEqual(workingCopy1, workingCopy2); - const workingCopy3 = await manager.resolve({ untitledResource: URI.file('/invalid/untitled') }); + const workingCopy3 = await manager.untitled.resolve({ untitledResource: URI.file('/invalid/untitled') }); assert.strictEqual(workingCopy3.resource.scheme, Schemas.untitled); workingCopy1.dispose(); @@ -161,7 +137,7 @@ suite('UntitledFileWorkingCopyManager', () => { }); test('resolve - with associated resource', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); assert.strictEqual(workingCopy.hasAssociatedFilePath, true); assert.strictEqual(workingCopy.resource.path, '/some/associated.txt'); @@ -170,7 +146,7 @@ suite('UntitledFileWorkingCopyManager', () => { }); test('save - without associated resource', async () => { - const workingCopy = await manager.resolve(); + const workingCopy = await manager.untitled.resolve(); workingCopy.model?.updateContents('Simple Save'); accessor.fileDialogService.setPickFileToSave(URI.file('simple/file.txt')); @@ -178,13 +154,13 @@ suite('UntitledFileWorkingCopyManager', () => { const result = await workingCopy.save(); assert.ok(result); - assert.strictEqual(manager.get(workingCopy.resource), undefined); + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); workingCopy.dispose(); }); test('save - with associated resource', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save with associated resource'); accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }), true); @@ -192,79 +168,26 @@ suite('UntitledFileWorkingCopyManager', () => { const result = await workingCopy.save(); assert.ok(result); - assert.strictEqual(manager.get(workingCopy.resource), undefined); + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); workingCopy.dispose(); }); test('save - with associated resource (asks to overwrite)', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save with associated resource'); let result = await workingCopy.save(); assert.ok(!result); // not confirmed - assert.strictEqual(manager.get(workingCopy.resource), workingCopy); + assert.strictEqual(manager.untitled.get(workingCopy.resource), workingCopy); accessor.dialogService.setConfirmResult({ confirmed: true }); result = await workingCopy.save(); assert.ok(result); // confirmed - assert.strictEqual(manager.get(workingCopy.resource), undefined); - - workingCopy.dispose(); - }); - - test('saveAs - without associated resource', async () => { - const workingCopy = await manager.resolve(); - workingCopy.model?.updateContents('Simple Save As'); - - const target = URI.file('simple/file.txt'); - accessor.fileDialogService.setPickFileToSave(target); - - const result = await manager.saveAs(workingCopy.resource, undefined); - assert.strictEqual(result?.resource.toString(), target.toString()); - - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); - - assert.strictEqual(manager.get(workingCopy.resource), undefined); - - workingCopy.dispose(); - }); - - test('saveAs - with associated resource', async () => { - const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); - workingCopy.model?.updateContents('Simple Save As with associated resource'); - - const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); - - accessor.fileService.notExistsSet.set(target, true); - - const result = await manager.saveAs(workingCopy.resource, undefined); - assert.strictEqual(result?.resource.toString(), target.toString()); - - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); - - assert.strictEqual(manager.get(workingCopy.resource), undefined); - - workingCopy.dispose(); - }); - - test('saveAs - target exists and is resolved', async () => { - const workingCopy = await manager.resolve(); - workingCopy.model?.updateContents('Simple Save As'); - - const target = URI.file('simple/file.txt'); - const targetFileWorkingCopy = await fileManager.resolve(target); - accessor.fileDialogService.setPickFileToSave(target); - - const result = await manager.saveAs(workingCopy.resource, undefined); - assert.strictEqual(result, targetFileWorkingCopy); - - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); - - assert.strictEqual(manager.get(workingCopy.resource), undefined); + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); workingCopy.dispose(); }); @@ -272,16 +195,16 @@ suite('UntitledFileWorkingCopyManager', () => { test('destroy', async () => { assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); - await manager.resolve(); - await manager.resolve(); - await manager.resolve(); + await manager.untitled.resolve(); + await manager.untitled.resolve(); + await manager.untitled.resolve(); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 3); - assert.strictEqual(manager.workingCopies.length, 3); + assert.strictEqual(manager.untitled.workingCopies.length, 3); - await manager.destroy(); + await manager.untitled.destroy(); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); - assert.strictEqual(manager.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); }); }); From 1cc2f71a1ff7ca80ce0e817e15b5fa3c90995627 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 09:17:03 +0200 Subject: [PATCH 099/270] untitled file working copy - tests for new unified manager --- .../browser/fileWorkingCopyManager2.test.ts | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts new file mode 100644 index 00000000000..dfa89d1b333 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { workbenchInstantiationService, TestServiceAccessor, TestInMemoryFileSystemProvider } from 'vs/workbench/test/browser/workbenchTestServices'; +import { FileWorkingCopy, IFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; +import { Schemas } from 'vs/base/common/network'; +import { IFileWorkingCopyManager2, FileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; +import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; +import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; + +suite('FileWorkingCopyManager2', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + let manager: IFileWorkingCopyManager2; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + + accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); + accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); + + manager = new FileWorkingCopyManager2( + 'testFileWorkingCopyType2', + new TestFileWorkingCopyModelFactory(), + new TestUntitledFileWorkingCopyModelFactory(), + accessor.fileService, + instantiationService, + accessor.fileDialogService, + accessor.workingCopyFileService, + accessor.uriIdentityService, + accessor.dialogService, + accessor.environmentService, + accessor.pathService + ); + }); + + teardown(() => { + manager.dispose(); + }); + + test('resolve', async () => { + const fileWorkingCopy = await manager.resolve(URI.file('/test.html')); + assert.ok(fileWorkingCopy instanceof FileWorkingCopy); + assert.strictEqual(await manager.files.resolve(fileWorkingCopy.resource), fileWorkingCopy); + + const untitledFileWorkingCopy = await manager.resolve(); + assert.ok(untitledFileWorkingCopy instanceof UntitledFileWorkingCopy); + assert.strictEqual(await manager.untitled.resolve({ untitledResource: untitledFileWorkingCopy.resource }), untitledFileWorkingCopy); + + fileWorkingCopy.dispose(); + untitledFileWorkingCopy.dispose(); + }); + + test('destroy', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + + await manager.resolve(URI.file('/test.html')); + await manager.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello Untitled')) }); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); + assert.strictEqual(manager.files.workingCopies.length, 1); + assert.strictEqual(manager.untitled.workingCopies.length, 1); + + await manager.destroy(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + assert.strictEqual(manager.files.workingCopies.length, 0); + assert.strictEqual(manager.untitled.workingCopies.length, 0); + }); + + // saveAs: unresolved source, unresolved target + + test('saveAs - file (same target, unresolved source, unresolved target)', () => { + const source = URI.file('/path/source.txt'); + + return testSaveAsFile(source, source, false, false); + }); + + test('saveAs - file (same target, different case, unresolved source, unresolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/SOURCE.txt'); + + return testSaveAsFile(source, target, false, false); + }); + + test('saveAs - file (different target, unresolved source, unresolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/target.txt'); + + return testSaveAsFile(source, target, false, false); + }); + + // saveAs: resolved source, unresolved target + + test('saveAs - file (same target, resolved source, unresolved target)', () => { + const source = URI.file('/path/source.txt'); + + return testSaveAsFile(source, source, true, false); + }); + + test('saveAs - file (same target, different case, resolved source, unresolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/SOURCE.txt'); + + return testSaveAsFile(source, target, true, false); + }); + + test('saveAs - file (different target, resolved source, unresolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/target.txt'); + + return testSaveAsFile(source, target, true, false); + }); + + // saveAs: unresolved source, resolved target + + test('saveAs - file (same target, unresolved source, resolved target)', () => { + const source = URI.file('/path/source.txt'); + + return testSaveAsFile(source, source, false, true); + }); + + test('saveAs - file (same target, different case, unresolved source, resolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/SOURCE.txt'); + + return testSaveAsFile(source, target, false, true); + }); + + test('saveAs - file (different target, unresolved source, resolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/target.txt'); + + return testSaveAsFile(source, target, false, true); + }); + + // saveAs: resolved source, resolved target + + test('saveAs - file (same target, resolved source, resolved target)', () => { + const source = URI.file('/path/source.txt'); + + return testSaveAsFile(source, source, true, true); + }); + + test('saveAs - file (different target, resolved source, resolved target)', async () => { + const source = URI.file('/path/source.txt'); + const target = URI.file('/path/target.txt'); + + return testSaveAsFile(source, target, true, true); + }); + + async function testSaveAsFile(source: URI, target: URI, resolveSource: boolean, resolveTarget: boolean) { + let sourceWorkingCopy: IFileWorkingCopy | undefined = undefined; + if (resolveSource) { + sourceWorkingCopy = await manager.resolve(source); + sourceWorkingCopy.model?.updateContents('hello world'); + assert.ok(sourceWorkingCopy.isDirty()); + } + + let targetWorkingCopy: IFileWorkingCopy | undefined = undefined; + if (resolveTarget) { + targetWorkingCopy = await manager.resolve(target); + targetWorkingCopy.model?.updateContents('hello world'); + assert.ok(targetWorkingCopy.isDirty()); + } + + const result = await manager.saveAs(source, target); + if (accessor.uriIdentityService.extUri.isEqual(source, target) && resolveSource) { + // if the uris are considered equal (different case on macOS/Windows) + // and the source is to be resolved, the resulting working copy resource + // will be the source resource because we consider file working copies + // the same in that case + assert.strictEqual(source.toString(), result?.resource.toString()); + } else { + if (resolveSource || resolveTarget) { + assert.strictEqual(target.toString(), result?.resource.toString()); + } else { + if (accessor.uriIdentityService.extUri.isEqual(source, target)) { + assert.strictEqual(undefined, result); + } else { + assert.strictEqual(target.toString(), result?.resource.toString()); + } + } + } + + if (resolveSource) { + assert.strictEqual(sourceWorkingCopy?.isDirty(), false); + } + + if (resolveTarget) { + assert.strictEqual(targetWorkingCopy?.isDirty(), false); + } + } + + test('saveAs - untitled (without associated resource)', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - untitled (with associated resource)', async () => { + const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); + workingCopy.model?.updateContents('Simple Save As with associated resource'); + + const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); + + accessor.fileService.notExistsSet.set(target, true); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result?.resource.toString(), target.toString()); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); + + test('saveAs - untitled (target exists and is resolved)', async () => { + const workingCopy = await manager.resolve(); + workingCopy.model?.updateContents('Simple Save As'); + + const target = URI.file('simple/file.txt'); + const targetFileWorkingCopy = await manager.resolve(target); + accessor.fileDialogService.setPickFileToSave(target); + + const result = await manager.saveAs(workingCopy.resource, undefined); + assert.strictEqual(result, targetFileWorkingCopy); + + assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + + assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); + + workingCopy.dispose(); + }); +}); From 06ee4764a4c917d17ba26407d9b94645eb7b9611 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 09:37:10 +0200 Subject: [PATCH 100/270] untitled file working copy - test :lipstick: --- .../test/browser/fileWorkingCopy.test.ts | 19 +++++++++++++++-- .../browser/fileWorkingCopyManager.test.ts | 21 +++++++++++++++++-- .../browser/fileWorkingCopyManager2.test.ts | 8 ------- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index e01b543faa1..cfbfbdfc664 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -495,8 +495,6 @@ suite('FileWorkingCopy', function () { accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); await workingCopy.save({ force: true }); - } catch (error) { - // error is expected } finally { accessor.fileService.writeShouldThrowError = undefined; } @@ -559,6 +557,23 @@ suite('FileWorkingCopy', function () { assert.strictEqual(workingCopy.isDirty(), false); }); + test('save (errors, bubbles up with `ignoreErrorHandler`)', async () => { + await workingCopy.resolve(); + + let error: Error | undefined = undefined; + try { + accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); + + await workingCopy.save({ force: true, ignoreErrorHandler: true }); + } catch (e) { + error = e; + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + + assert.ok(error); + }); + test('revert', async () => { await workingCopy.resolve(); workingCopy.model?.updateContents('hello revert'); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 9905e3ac630..a0a4199ff0b 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -10,7 +10,7 @@ import { workbenchInstantiationService, TestServiceAccessor, TestWillShutdownEve import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; -import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; +import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { timeout } from 'vs/base/common/async'; import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -189,6 +189,7 @@ suite('FileWorkingCopyManager', () => { let gotNonDirtyCounter = 0; let revertedCounter = 0; let savedCounter = 0; + let saveErrorCounter = 0; manager.onDidCreate(workingCopy => { createdCounter++; @@ -222,6 +223,12 @@ suite('FileWorkingCopyManager', () => { } }); + manager.onDidSaveError(workingCopy => { + if (workingCopy.resource.toString() === resource1.toString()) { + saveErrorCounter++; + } + }); + const workingCopy1 = await manager.resolve(resource1); assert.strictEqual(resolvedCounter, 1); assert.strictEqual(createdCounter, 1); @@ -239,14 +246,24 @@ suite('FileWorkingCopyManager', () => { workingCopy1.model?.updateContents('changed again'); await workingCopy1.save(); + + try { + accessor.fileService.writeShouldThrowError = new FileOperationError('write error', FileOperationResult.FILE_PERMISSION_DENIED); + + await workingCopy1.save({ force: true }); + } finally { + accessor.fileService.writeShouldThrowError = undefined; + } + workingCopy1.dispose(); workingCopy2.dispose(); await workingCopy1.revert(); - assert.strictEqual(gotDirtyCounter, 2); + assert.strictEqual(gotDirtyCounter, 3); assert.strictEqual(gotNonDirtyCounter, 2); assert.strictEqual(revertedCounter, 1); assert.strictEqual(savedCounter, 1); + assert.strictEqual(saveErrorCounter, 1); assert.strictEqual(createdCounter, 2); workingCopy1.dispose(); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts index dfa89d1b333..ae6a5510c04 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts @@ -78,8 +78,6 @@ suite('FileWorkingCopyManager2', () => { assert.strictEqual(manager.untitled.workingCopies.length, 0); }); - // saveAs: unresolved source, unresolved target - test('saveAs - file (same target, unresolved source, unresolved target)', () => { const source = URI.file('/path/source.txt'); @@ -100,8 +98,6 @@ suite('FileWorkingCopyManager2', () => { return testSaveAsFile(source, target, false, false); }); - // saveAs: resolved source, unresolved target - test('saveAs - file (same target, resolved source, unresolved target)', () => { const source = URI.file('/path/source.txt'); @@ -122,8 +118,6 @@ suite('FileWorkingCopyManager2', () => { return testSaveAsFile(source, target, true, false); }); - // saveAs: unresolved source, resolved target - test('saveAs - file (same target, unresolved source, resolved target)', () => { const source = URI.file('/path/source.txt'); @@ -144,8 +138,6 @@ suite('FileWorkingCopyManager2', () => { return testSaveAsFile(source, target, false, true); }); - // saveAs: resolved source, resolved target - test('saveAs - file (same target, resolved source, resolved target)', () => { const source = URI.file('/path/source.txt'); From 99f3a3726eb8d24e8036ce5a6d7fbad122b3a3d8 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 18 May 2021 14:38:56 +0200 Subject: [PATCH 101/270] Additional details in the aria label --- .../contrib/workspace/browser/workspace.contribution.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 148c23121e1..a41c0bdbd69 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -202,6 +202,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben return { id: BANNER_RESTRICTED_MODE, icon: shieldIcon, + ariaLabel: localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use left, right navigation keys to access banner actions."), message: localize('restrictedModeBannerMessage', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."), actions: [ { From e5bf21393bfce953cec372323458d27b71485f3f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 19 May 2021 09:51:15 +0200 Subject: [PATCH 102/270] Additional navigation keys --- src/vs/workbench/browser/parts/banner/bannerPart.ts | 2 ++ .../contrib/workspace/browser/workspace.contribution.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 1665bb78406..4a042ca9d19 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -288,6 +288,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.banner.focusNextAction', weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.RightArrow, + secondary: [KeyCode.DownArrow], when: CONTEXT_BANNER_FOCUSED, handler: (accessor: ServicesAccessor) => { const bannerService = accessor.get(IBannerService); @@ -299,6 +300,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.banner.focusPreviousAction', weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.LeftArrow, + secondary: [KeyCode.UpArrow], when: CONTEXT_BANNER_FOCUSED, handler: (accessor: ServicesAccessor) => { const bannerService = accessor.get(IBannerService); diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index a41c0bdbd69..902e74d93d5 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -202,7 +202,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben return { id: BANNER_RESTRICTED_MODE, icon: shieldIcon, - ariaLabel: localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use left, right navigation keys to access banner actions."), + ariaLabel: localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use navigation keys to access banner actions."), message: localize('restrictedModeBannerMessage', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."), actions: [ { From 08618458110ecad754f73d3607467c9c1854ce76 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 19 May 2021 10:03:43 +0200 Subject: [PATCH 103/270] fix ExtensionRecommendationsService test (for #124119) --- .../extensionRecommendationsService.ts | 10 +++++-- .../extensionRecommendationsService.test.ts | 30 +++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 56aff6ead14..b353d267df2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -28,8 +28,6 @@ type IgnoreRecommendationClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; }; -const WORKSPACE_RECOMMENDATION_DELAY = 5000; - export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService { declare readonly _serviceBrand: undefined; @@ -235,13 +233,19 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return !this.extensionRecommendationsManagementService.ignoredRecommendations.includes(extensionId.toLowerCase()); } + // for testing + protected get workbenchRecommendationDelay() { + // remote extensions might still being installed #124119 + return 5000; + } + private async promptWorkspaceRecommendations(): Promise { const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations] .map(({ extensionId }) => extensionId) .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); if (allowedRecommendations.length) { - await timeout(WORKSPACE_RECOMMENDATION_DELAY); // remote extensions might still being installed #124119 + await timeout(this.workbenchRecommendationDelay); await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations); } } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index efedc1c28dd..9798f435ebf 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -175,6 +175,12 @@ function aGalleryExtension(name: string, properties: any = {}, galleryExtensionP return galleryExtension; } +class TestExtensionRecommendationsService extends ExtensionRecommendationsService { + protected override get workbenchRecommendationDelay() { + return 0; + } +} + suite('ExtensionRecommendationsService Test', () => { let workspaceService: IWorkspaceContextService; let instantiationService: TestInstantiationService; @@ -311,7 +317,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.strictEqual(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); @@ -321,7 +327,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptOrRecommendationsForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); assert.ok(!prompted); return testObject.getWorkspaceRecommendations().then(() => { @@ -350,7 +356,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => { await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await Event.toPromise(promptedEmitter.event); const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); @@ -379,7 +385,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if ignoreRecommendations is set', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -389,7 +395,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if showRecommendationsOnlyOnDemand is set', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { assert.ok(!prompted); }); @@ -407,7 +413,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored @@ -425,7 +431,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored @@ -447,7 +453,7 @@ suite('ExtensionRecommendationsService Test', () => { storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await testObject.activationPromise; const recommendations = testObject.getAllRecommendationsWithReason(); @@ -467,7 +473,7 @@ suite('ExtensionRecommendationsService Test', () => { await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); await testObject.activationPromise; let recommendations = testObject.getAllRecommendationsWithReason(); @@ -499,7 +505,7 @@ suite('ExtensionRecommendationsService Test', () => { storageService.store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.GLOBAL, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', []); - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); extensionIgnoredRecommendationsService.onDidChangeGlobalIgnoredRecommendation(changeHandlerTarget); extensionIgnoredRecommendationsService.toggleGlobalIgnoredRecommendation(ignoredExtensionId, true); @@ -514,7 +520,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); @@ -533,7 +539,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); + testObject = instantiationService.createInstance(TestExtensionRecommendationsService); return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.strictEqual(recommendations.length, 2); From 734b79dadae2caa8b6b3337faa31451d6e85c01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 19 May 2021 10:38:47 +0200 Subject: [PATCH 104/270] fix #124102 --- src/vs/workbench/contrib/update/browser/update.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index e294db7c023..473dbf7f0d9 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -275,10 +275,18 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu if (state.type === StateType.AvailableForDownload || state.type === StateType.Downloaded || state.type === StateType.Ready) { badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameShort)); - } else if (state.type === StateType.CheckingForUpdates || state.type === StateType.Downloading || state.type === StateType.Updating) { + } else if (state.type === StateType.CheckingForUpdates) { badge = new ProgressBadge(() => nls.localize('checkingForUpdates', "Checking for Updates...")); clazz = 'progress-badge'; priority = 1; + } else if (state.type === StateType.Downloading) { + badge = new ProgressBadge(() => nls.localize('downloading', "Downloading...")); + clazz = 'progress-badge'; + priority = 1; + } else if (state.type === StateType.Updating) { + badge = new ProgressBadge(() => nls.localize('updating', "Updating...")); + clazz = 'progress-badge'; + priority = 1; } this.badgeDisposable.clear(); From f23f011f5528a93d2d2c1262af1941ec7e26db22 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 11:38:02 +0200 Subject: [PATCH 105/270] untitled - clarify resolve methods --- .../common/fileWorkingCopyManager2.ts | 46 ++++++++++++------- .../common/untitledFileWorkingCopyManager.ts | 44 ++++++++++++------ .../browser/fileWorkingCopyManager2.test.ts | 2 +- .../untitledFileWorkingCopyManager.test.ts | 16 ++++++- 4 files changed, 75 insertions(+), 33 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts index b6c1944af3a..32b0932927d 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts @@ -20,7 +20,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; -import { IExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyWithAssociatedResourceOptions, IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; +import { INewOrExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyWithAssociatedResourceOptions, IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { isValidBasename } from 'vs/base/common/extpath'; @@ -36,18 +36,6 @@ export interface IFileWorkingCopyManager2; - /** - * Resolves an untitled file working copy from the provided options. - */ - resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; - resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; - - /** - * Resolves an untitled file working copy from the provided options - * unless an existing working copy already exists with that resource. - */ - resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; - /** * Allows to resolve a file working copy. If the manager already knows * about a file working copy with the same `URI`, it will return that @@ -63,10 +51,34 @@ export interface IFileWorkingCopyManager2>; + /** + * Create a new untitled file working copy with optional initial contents. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + + /** + * Create a new untitled file working copy with optional initial contents + * and associated resource. The associated resource will be used when + * saving and will not require to ask the user for a file path. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + + /** + * Creates a new untitled file working copy with optional initial contents + * with the provided resource or return an existing untitled file working + * copy otherwise. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; + /** * Implements "Save As" for file based working copies. The API is `URI` based * because it works even without resolved file working copies. If a file working @@ -81,6 +93,8 @@ export interface IFileWorkingCopyManager2>; resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; - resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; - resolve(arg1?: URI | INewUntitledFileWorkingCopyOptions | INewUntitledFileWorkingCopyWithAssociatedResourceOptions | IExistingUntitledFileWorkingCopyOptions, arg2?: IFileWorkingCopyResolveOptions): Promise | IFileWorkingCopy> { + resolve(arg1?: URI | INewUntitledFileWorkingCopyOptions | INewUntitledFileWorkingCopyWithAssociatedResourceOptions | INewOrExistingUntitledFileWorkingCopyOptions, arg2?: IFileWorkingCopyResolveOptions): Promise | IFileWorkingCopy> { if (URI.isUri(arg1)) { return this.files.resolve(arg1, arg2); } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 09562661f3d..8b326b5dadd 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -35,16 +35,29 @@ export interface IUntitledFileWorkingCopyManager>; /** - * Resolves an untitled file working copy from the provided options. + * Create a new untitled file working copy with optional initial contents. + * + * Note: Callers must `dispose` the working copy when no longer needed. */ resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + + /** + * Create a new untitled file working copy with optional initial contents + * and associated resource. The associated resource will be used when + * saving and will not require to ask the user for a file path. + * + * Note: Callers must `dispose` the working copy when no longer needed. + */ resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; /** - * Resolves an untitled file working copy from the provided options - * unless an existing working copy already exists with that resource. + * Creates a new untitled file working copy with optional initial contents + * with the provided resource or return an existing untitled file working + * copy otherwise. + * + * Note: Callers must `dispose` the working copy when no longer needed. */ - resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; } export interface INewUntitledFileWorkingCopyOptions { @@ -55,7 +68,7 @@ export interface INewUntitledFileWorkingCopyOptions { * Note: An untitled file working copy with initial * value is dirty right from the beginning. */ - initialValue?: VSBufferReadableStream; + contents?: VSBufferReadableStream; } export interface INewUntitledFileWorkingCopyWithAssociatedResourceOptions extends INewUntitledFileWorkingCopyOptions { @@ -71,7 +84,7 @@ export interface INewUntitledFileWorkingCopyWithAssociatedResourceOptions extend associatedResource: { authority?: string; path?: string; query?: string; fragment?: string; } } -export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFileWorkingCopyOptions { +export interface INewOrExistingUntitledFileWorkingCopyOptions extends INewUntitledFileWorkingCopyOptions { /** * A resource to identify the untitled file working copy @@ -82,7 +95,7 @@ export interface IExistingUntitledFileWorkingCopyOptions extends INewUntitledFil untitledResource: URI; } -type IInternalUntitledFileWorkingCopyOptions = INewUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions & IExistingUntitledFileWorkingCopyOptions; +type IInternalUntitledFileWorkingCopyOptions = INewUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions & INewOrExistingUntitledFileWorkingCopyOptions; export class UntitledFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IUntitledFileWorkingCopyManager { @@ -115,7 +128,7 @@ export class UntitledFileWorkingCopyManager>; resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; - resolve(options?: IExistingUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { const workingCopy = this.doCreateOrGet(options); await workingCopy.resolve(); @@ -141,7 +154,7 @@ export class UntitledFileWorkingCopyManager; diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts index ae6a5510c04..9ec0f1f82b2 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts @@ -65,7 +65,7 @@ suite('FileWorkingCopyManager2', () => { assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); await manager.resolve(URI.file('/test.html')); - await manager.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello Untitled')) }); + await manager.resolve({ contents: bufferToStream(VSBuffer.fromString('Hello Untitled')) }); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); assert.strictEqual(manager.files.workingCopies.length, 1); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 8a7b56c90d3..5acba50cecc 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -113,7 +113,7 @@ suite('UntitledFileWorkingCopyManager', () => { dirtyCounter++; }); - const workingCopy = await manager.untitled.resolve({ initialValue: bufferToStream(VSBuffer.fromString('Hello World')) }); + const workingCopy = await manager.untitled.resolve({ contents: bufferToStream(VSBuffer.fromString('Hello World')) }); assert.strictEqual(workingCopy.isDirty(), true); assert.strictEqual(dirtyCounter, 1); @@ -136,6 +136,20 @@ suite('UntitledFileWorkingCopyManager', () => { workingCopy3.dispose(); }); + test('resolve - untitled resource used for new working copy', async () => { + const invalidUntitledResource = URI.file('my/untitled.txt'); + const validUntitledResource = invalidUntitledResource.with({ scheme: Schemas.untitled }); + + const workingCopy1 = await manager.untitled.resolve({ untitledResource: invalidUntitledResource }); + assert.notStrictEqual(workingCopy1.resource.toString(), invalidUntitledResource.toString()); + + const workingCopy2 = await manager.untitled.resolve({ untitledResource: validUntitledResource }); + assert.strictEqual(workingCopy2.resource.toString(), validUntitledResource.toString()); + + workingCopy1.dispose(); + workingCopy2.dispose(); + }); + test('resolve - with associated resource', async () => { const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); From 159479eb5ae451a66b5dac3c12d564f32f454796 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 14:03:28 +0200 Subject: [PATCH 106/270] Allow to show status bar entries with an id and name (fix #74972) --- extensions/debug-auto-launch/src/extension.ts | 8 +- .../github-authentication/src/githubServer.ts | 3 +- .../src/binarySizeStatusBarEntry.ts | 7 +- .../image-preview/src/ownedStatusBarEntry.ts | 5 +- .../image-preview/src/sizeStatusBarEntry.ts | 7 +- .../image-preview/src/zoomStatusBarEntry.ts | 7 +- .../client/src/jsonClient.ts | 8 +- .../src/tsServer/versionStatus.ts | 8 +- .../src/utils/largeProjectStatus.ts | 8 +- .../src/singlefolder-tests/window.test.ts | 20 ++++- src/vs/vscode.d.ts | 23 ++++++ src/vs/vscode.proposed.d.ts | 45 ----------- .../api/browser/mainThreadStatusBar.ts | 10 +-- .../workbench/api/common/extHost.api.impl.ts | 23 +++--- .../workbench/api/common/extHostStatusBar.ts | 74 +++++++++++++++---- .../browser/parts/editor/editorStatus.ts | 35 +++++---- .../notifications/notificationsStatus.ts | 9 ++- .../browser/parts/statusbar/statusbarPart.ts | 26 ++++--- .../contrib/debug/browser/debugStatus.ts | 3 +- .../extensionProfileService.ts | 3 +- .../feedback/browser/feedbackStatusbarItem.ts | 3 +- .../markers/browser/markers.contribution.ts | 3 +- .../browser/contrib/status/editorStatus.ts | 7 +- .../browser/keyboardLayoutPicker.ts | 7 +- .../contrib/remote/browser/remoteExplorer.ts | 3 +- .../contrib/remote/browser/remoteIndicator.ts | 3 +- .../workbench/contrib/scm/browser/activity.ts | 3 +- .../tasks/browser/task.contribution.ts | 3 +- .../browser/workspace.contribution.ts | 3 +- .../progress/browser/progressService.ts | 3 +- .../services/statusbar/common/statusbar.ts | 9 ++- 31 files changed, 212 insertions(+), 167 deletions(-) diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts index b47641f2db2..1016f0af44a 100644 --- a/extensions/debug-auto-launch/src/extension.ts +++ b/extensions/debug-auto-launch/src/extension.ts @@ -315,12 +315,10 @@ function updateStatusBar(context: vscode.ExtensionContext, state: State, busy = } if (!statusItem) { - statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + statusItem = vscode.window.createStatusBarItem('status.debug.autoAttach', vscode.StatusBarAlignment.Left); + statusItem.name = localize('status.name.auto.attach', "Debug Auto Attach"); statusItem.command = TOGGLE_COMMAND; - statusItem.tooltip = localize( - 'status.tooltip.auto.attach', - 'Automatically attach to node.js processes in debug mode', - ); + statusItem.tooltip = localize('status.tooltip.auto.attach', "Automatically attach to node.js processes in debug mode"); context.subscriptions.push(statusItem); } diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index fa81fd97d5d..a3124b7c64b 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -179,7 +179,8 @@ export class GitHubServer { private updateStatusBarItem(isStart?: boolean) { if (isStart && !this._statusBarItem) { - this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + this._statusBarItem = vscode.window.createStatusBarItem('status.git.signIn', vscode.StatusBarAlignment.Left); + this._statusBarItem.name = localize('status.git.signIn.name', "GitHub Sign-in"); this._statusBarItem.text = this.type === AuthProviderType.github ? localize('signingIn', "$(mark-github) Signing in to github.com...") : localize('signingInEnterprise', "$(mark-github) Signing in to {0}...", this.getServerUri().authority); diff --git a/extensions/image-preview/src/binarySizeStatusBarEntry.ts b/extensions/image-preview/src/binarySizeStatusBarEntry.ts index 0c983d37cd2..ce375fc19b8 100644 --- a/extensions/image-preview/src/binarySizeStatusBarEntry.ts +++ b/extensions/image-preview/src/binarySizeStatusBarEntry.ts @@ -39,12 +39,7 @@ class BinarySize { export class BinarySizeStatusBarEntry extends PreviewStatusBarEntry { constructor() { - super({ - id: 'imagePreview.binarySize', - name: localize('sizeStatusBar.name', "Image Binary Size"), - alignment: vscode.StatusBarAlignment.Right, - priority: 100, - }); + super('status.imagePreview.binarySize', localize('sizeStatusBar.name', "Image Binary Size"), vscode.StatusBarAlignment.Right, 100); } public show(owner: string, size: number | undefined) { diff --git a/extensions/image-preview/src/ownedStatusBarEntry.ts b/extensions/image-preview/src/ownedStatusBarEntry.ts index 51c9e25503c..31165f67d69 100644 --- a/extensions/image-preview/src/ownedStatusBarEntry.ts +++ b/extensions/image-preview/src/ownedStatusBarEntry.ts @@ -11,9 +11,10 @@ export abstract class PreviewStatusBarEntry extends Disposable { protected readonly entry: vscode.StatusBarItem; - constructor(options: vscode.StatusBarItemOptions) { + constructor(id: string, name: string, alignment: vscode.StatusBarAlignment, priority: number) { super(); - this.entry = this._register(vscode.window.createStatusBarItem(options)); + this.entry = this._register(vscode.window.createStatusBarItem(id, alignment, priority)); + this.entry.name = name; } protected showItem(owner: string, text: string) { diff --git a/extensions/image-preview/src/sizeStatusBarEntry.ts b/extensions/image-preview/src/sizeStatusBarEntry.ts index e74eea0fe60..68e5c34d232 100644 --- a/extensions/image-preview/src/sizeStatusBarEntry.ts +++ b/extensions/image-preview/src/sizeStatusBarEntry.ts @@ -12,12 +12,7 @@ const localize = nls.loadMessageBundle(); export class SizeStatusBarEntry extends PreviewStatusBarEntry { constructor() { - super({ - id: 'imagePreview.size', - name: localize('sizeStatusBar.name', "Image Size"), - alignment: vscode.StatusBarAlignment.Right, - priority: 101 /* to the left of editor status (100) */, - }); + super('status.imagePreview.size', localize('sizeStatusBar.name', "Image Size"), vscode.StatusBarAlignment.Right, 101 /* to the left of editor status (100) */); } public show(owner: string, text: string) { diff --git a/extensions/image-preview/src/zoomStatusBarEntry.ts b/extensions/image-preview/src/zoomStatusBarEntry.ts index 18adc19d6d2..a4fcdc2b604 100644 --- a/extensions/image-preview/src/zoomStatusBarEntry.ts +++ b/extensions/image-preview/src/zoomStatusBarEntry.ts @@ -19,12 +19,7 @@ export class ZoomStatusBarEntry extends OwnedStatusBarEntry { public readonly onDidChangeScale = this._onDidChangeScale.event; constructor() { - super({ - id: 'imagePreview.zoom', - name: localize('zoomStatusBar.name', "Image Zoom"), - alignment: vscode.StatusBarAlignment.Right, - priority: 102 /* to the left of editor size entry (101) */, - }); + super('status.imagePreview.zoom', localize('zoomStatusBar.name', "Image Zoom"), vscode.StatusBarAlignment.Right, 102 /* to the left of editor size entry (101) */); this._register(vscode.commands.registerCommand(selectZoomLevelCommandId, async () => { type MyPickItem = vscode.QuickPickItem & { scale: Scale }; diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index c925a9181f6..611327188ef 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -101,12 +101,8 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua const documentSelector = ['json', 'jsonc']; - const schemaResolutionErrorStatusBarItem = window.createStatusBarItem({ - id: 'status.json.resolveError', - name: localize('json.resolveError', "JSON: Schema Resolution Error"), - alignment: StatusBarAlignment.Right, - priority: 0, - }); + const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0); + schemaResolutionErrorStatusBarItem.name = localize('json.resolveError', "JSON: Schema Resolution Error"); schemaResolutionErrorStatusBarItem.text = '$(alert)'; toDispose.push(schemaResolutionErrorStatusBarItem); diff --git a/extensions/typescript-language-features/src/tsServer/versionStatus.ts b/extensions/typescript-language-features/src/tsServer/versionStatus.ts index a868c3e2502..2560f3e8be9 100644 --- a/extensions/typescript-language-features/src/tsServer/versionStatus.ts +++ b/extensions/typescript-language-features/src/tsServer/versionStatus.ts @@ -135,12 +135,8 @@ export default class VersionStatus extends Disposable { ) { super(); - this._statusBarEntry = this._register(vscode.window.createStatusBarItem({ - id: 'status.typescript', - name: localize('projectInfo.name', "TypeScript: Project Info"), - alignment: vscode.StatusBarAlignment.Right, - priority: 99 /* to the right of editor status (100) */ - })); + this._statusBarEntry = this._register(vscode.window.createStatusBarItem('status.typescript', vscode.StatusBarAlignment.Right, 99 /* to the right of editor status (100) */)); + this._statusBarEntry.name = localize('projectInfo.name', "TypeScript: Project Info"); const command = new ProjectStatusCommand(this._client, () => this._state); commandManager.register(command); diff --git a/extensions/typescript-language-features/src/utils/largeProjectStatus.ts b/extensions/typescript-language-features/src/utils/largeProjectStatus.ts index 223d7cb4716..346f95c6979 100644 --- a/extensions/typescript-language-features/src/utils/largeProjectStatus.ts +++ b/extensions/typescript-language-features/src/utils/largeProjectStatus.ts @@ -23,12 +23,8 @@ class ExcludeHintItem { constructor( private readonly telemetryReporter: TelemetryReporter ) { - this._item = vscode.window.createStatusBarItem({ - id: 'status.typescript.exclude', - name: localize('statusExclude', "TypeScript: Configure Excludes"), - alignment: vscode.StatusBarAlignment.Right, - priority: 98 /* to the right of typescript version status (99) */ - }); + this._item = vscode.window.createStatusBarItem('status.typescript.exclude', vscode.StatusBarAlignment.Right, 98 /* to the right of typescript version status (99) */); + this._item.name = localize('statusExclude', "TypeScript: Configure Excludes"); this._item.command = 'js.projectStatus.command'; } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index b672c7705ac..0a403549ce2 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind, QuickPickItem, TextEditor } from 'vscode'; +import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind, QuickPickItem, TextEditor, StatusBarAlignment } from 'vscode'; import { join } from 'path'; import { closeAllEditors, pathEquals, createRandomFile, assertNoRpc } from '../utils'; @@ -638,4 +638,22 @@ suite('vscode API - window', () => { }); }); + + test('createStatusBar', async function () { + const statusBarEntryWithoutId = window.createStatusBarItem(StatusBarAlignment.Left, 100); + assert.strictEqual(statusBarEntryWithoutId.id, 'vscode.vscode-api-tests'); + assert.strictEqual(statusBarEntryWithoutId.alignment, StatusBarAlignment.Left); + assert.strictEqual(statusBarEntryWithoutId.priority, 100); + assert.strictEqual(statusBarEntryWithoutId.name, undefined); + statusBarEntryWithoutId.name = 'Test Name'; + assert.strictEqual(statusBarEntryWithoutId.name, 'Test Name'); + + const statusBarEntryWithId = window.createStatusBarItem('testId', StatusBarAlignment.Right, 200); + assert.strictEqual(statusBarEntryWithId.alignment, StatusBarAlignment.Right); + assert.strictEqual(statusBarEntryWithId.priority, 200); + assert.strictEqual(statusBarEntryWithId.id, 'testId'); + assert.strictEqual(statusBarEntryWithId.name, undefined); + statusBarEntryWithId.name = 'Test Name'; + assert.strictEqual(statusBarEntryWithId.name, 'Test Name'); + }); }); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 40cf4af251f..e2d046b0db8 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5584,6 +5584,14 @@ declare module 'vscode' { */ export interface StatusBarItem { + /** + * The identifier of this item. + * + * *Note*: if no identifier was provided by the {@link window.createStatusBarItem `window.createStatusBarItem`} + * method, the identifier will match the {@link Extension.id extension identifier}. + */ + readonly id: string; + /** * The alignment of this item. */ @@ -5595,6 +5603,11 @@ declare module 'vscode' { */ readonly priority?: number; + /** + * The (short) name of the entry, like 'Language Indicator', 'Git Status' etc. + */ + name: string | undefined; + /** * The text to show for the entry. You can embed icons in the text by leveraging the syntax: * @@ -8804,6 +8817,16 @@ declare module 'vscode' { */ export function createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + /** + * Creates a status bar {@link StatusBarItem item}. + * + * @param id The unique identifier of the item. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createStatusBarItem(id: string, alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + /** * Creates a {@link Terminal} with a backing shell process. The cwd of the terminal will be the workspace * directory if it exists. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 147fcfb5ad6..73a0d360a47 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -938,51 +938,6 @@ declare module 'vscode' { } //#endregion - //#region Status bar item with ID and Name: https://github.com/microsoft/vscode/issues/74972 - - /** - * Options to configure the status bar item. - */ - export interface StatusBarItemOptions { - - /** - * An identifier for the item that should be unique. - */ - id: string; - - /** - * A human readable name of the item that explains the purpose - * of the item to the user. - */ - name: string; - - /** - * The alignment of the item. - */ - alignment?: StatusBarAlignment; - - /** - * The priority of the item. Higher values mean the item should be shown more to the left. - */ - priority?: number; - } - - export namespace window { - - /** - * Creates a status bar {@link StatusBarItem item}. - * - * @param options The options of the item. If not provided, some default values - * will be assumed. For example, the {@link StatusBarItemOptions.id `StatusBarItemOptions.id`} - * will be the id of the extension and the {@link StatusBarItemOptions.name `StatusBarItemOptions.name`} - * will be the extension name. - * @return A new status bar item. - */ - export function createStatusBarItem(options?: StatusBarItemOptions): StatusBarItem; - } - - //#endregion - //#region Custom editor move https://github.com/microsoft/vscode/issues/86146 // TODO: Also for custom editor diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 3ba387186db..69f8d2cf92a 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -27,7 +27,7 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this.entries.clear(); } - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void { + $setEntry(entryId: number, id: string, name: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, backgroundColor: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void { // if there are icons in the text use the tooltip for the aria label let ariaLabel: string; let role: string | undefined = undefined; @@ -37,23 +37,23 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { } else { ariaLabel = getCodiconAriaLabel(text); } - const entry: IStatusbarEntry = { text, tooltip, command, color, backgroundColor, ariaLabel, role }; + const entry: IStatusbarEntry = { name, text, tooltip, command, color, backgroundColor, ariaLabel, role }; if (typeof priority === 'undefined') { priority = 0; } // Reset existing entry if alignment or priority changed - let existingEntry = this.entries.get(id); + let existingEntry = this.entries.get(entryId); if (existingEntry && (existingEntry.alignment !== alignment || existingEntry.priority !== priority)) { dispose(existingEntry.accessor); - this.entries.delete(id); + this.entries.delete(entryId); existingEntry = undefined; } // Create new entry if not existing if (!existingEntry) { - this.entries.set(id, { accessor: this.statusbarService.addEntry(entry, statusId, statusName, alignment, priority), alignment, priority }); + this.entries.set(entryId, { accessor: this.statusbarService.addEntry(entry, id, alignment, priority), alignment, priority }); } // Otherwise update diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e0f69afce4c..5c5f06be704 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -597,23 +596,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showSaveDialog(options) { return extHostDialogs.showSaveDialog(options); }, - createStatusBarItem(alignmentOrOptions?: vscode.StatusBarAlignment | vscode.StatusBarItemOptions, priority?: number): vscode.StatusBarItem { - let id: string; - let name: string; + createStatusBarItem(alignmentOrId?: vscode.StatusBarAlignment | string, priorityOrAlignment?: number | vscode.StatusBarAlignment, priorityArg?: number): vscode.StatusBarItem { + let id: string | undefined; let alignment: number | undefined; + let priority: number | undefined; - if (alignmentOrOptions && typeof alignmentOrOptions !== 'number') { - id = alignmentOrOptions.id; - name = alignmentOrOptions.name; - alignment = alignmentOrOptions.alignment; - priority = alignmentOrOptions.priority; + if (typeof alignmentOrId === 'string') { + id = alignmentOrId; + alignment = priorityOrAlignment; + priority = priorityArg; } else { - id = extension.identifier.value; - name = nls.localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name); - alignment = alignmentOrOptions; + alignment = alignmentOrId; + priority = priorityOrAlignment; } - return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority); + return extHostStatusBar.createStatusBarEntry(extension, id, alignment, priority); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index 9c875cdc084..f85f182011e 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -10,8 +10,10 @@ import { MainContext, MainThreadStatusBarShape, IMainContext, ICommandDto } from import { localize } from 'vs/nls'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostStatusBarEntry implements vscode.StatusBarItem { + private static ID_GEN = 0; private static ALLOWED_BACKGROUND_COLORS = new Map( @@ -21,17 +23,20 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { #proxy: MainThreadStatusBarShape; #commands: CommandsConverter; - private _id: number; + private _entryId: number; + + private _extension?: IExtensionDescription; + + private _id?: string; private _alignment: number; private _priority?: number; + private _disposed: boolean = false; private _visible: boolean = false; - private _statusId: string; - private _statusName: string; - private _text: string = ''; private _tooltip?: string; + private _name?: string; private _color?: string | ThemeColor; private _backgroundColor?: ThemeColor; private readonly _internalCommandRegistration = new DisposableStore(); @@ -43,19 +48,23 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _timeoutHandle: any; private _accessibilityInformation?: vscode.AccessibilityInformation; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number); + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, extension?: IExtensionDescription, id?: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { this.#proxy = proxy; this.#commands = commands; - this._id = ExtHostStatusBarEntry.ID_GEN++; - this._statusId = id; - this._statusName = name; + this._entryId = ExtHostStatusBarEntry.ID_GEN++; + + this._extension = extension; + + this._id = id; this._alignment = alignment; this._priority = priority; } - public get id(): number { - return this._id; + public get id(): string { + return this._id ?? this._extension!.identifier.value; } public get alignment(): vscode.StatusBarAlignment { @@ -70,6 +79,10 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._text; } + public get name(): string | undefined { + return this._name; + } + public get tooltip(): string | undefined { return this._tooltip; } @@ -95,6 +108,11 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this.update(); } + public set name(name: string | undefined) { + this._name = name; + this.update(); + } + public set tooltip(tooltip: string | undefined) { this._tooltip = tooltip; this.update(); @@ -149,7 +167,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { public hide(): void { clearTimeout(this._timeoutHandle); this._visible = false; - this.#proxy.$dispose(this.id); + this.#proxy.$dispose(this._entryId); } private update(): void { @@ -163,6 +181,28 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._timeoutHandle = setTimeout(() => { this._timeoutHandle = undefined; + // If the id is not set, derive it from the extension identifier, + // otherwise make sure to prefix it with the extension identifier + // to get a more unique value across extensions. + let id: string; + if (this._extension) { + if (this._id) { + id = `${this._extension.identifier.value}.${this._id}`; + } else { + id = this._extension.identifier.value; + } + } else { + id = this._id!; + } + + // If the name is not set, derive it from the extension descriptor + let name: string; + if (this._name) { + name = this._name; + } else { + name = localize('extensionLabel', "{0} (Extension)", this._extension!.displayName || this._extension!.name); + } + // If a background color is set, the foreground is determined let color = this._color; if (this._backgroundColor) { @@ -170,7 +210,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } // Set to status bar - this.#proxy.$setEntry(this.id, this._statusId, this._statusName, this._text, this._tooltip, this._command?.internal, color, + this.#proxy.$setEntry(this._entryId, id, name, this._text, this._tooltip, this._command?.internal, color, this._backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, this._priority, this._accessibilityInformation); }, 0); @@ -188,7 +228,8 @@ class StatusBarMessage { private _messages: { message: string }[] = []; constructor(statusBar: ExtHostStatusBar) { - this._item = statusBar.createStatusBarEntry('status.extensionMessage', localize('status.extensionMessage', "Extension Status"), ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); + this._item = statusBar.createStatusBarEntry(undefined, 'status.extensionMessage', ExtHostStatusBarAlignment.Left, Number.MIN_VALUE); + this._item.name = localize('status.extensionMessage', "Extension Status"); } dispose() { @@ -232,12 +273,13 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority); + createStatusBarEntry(extension: IExtensionDescription | undefined, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; + createStatusBarEntry(extension: IExtensionDescription, id?: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem; + createStatusBarEntry(extension: IExtensionDescription, id: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, extension, id, alignment, priority); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { - const d = this._statusMessage.setMessage(text); let handle: any; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 4c2e352c455..75d2c9bc6a4 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -404,13 +404,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.tabFocusModeElement.value) { const text = localize('tabFocusModeEnabled', "Tab Moves Focus"); this.tabFocusModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.tabFocusMode', "Accessibility Mode"), text, ariaLabel: text, tooltip: localize('disableTabMode', "Disable Accessibility Mode"), command: 'editor.action.toggleTabFocusMode', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.tabFocusMode', localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7); + }, 'status.editor.tabFocusMode', StatusbarAlignment.RIGHT, 100.7); } } else { this.tabFocusModeElement.clear(); @@ -422,13 +423,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.columnSelectionModeElement.value) { const text = localize('columnSelectionModeEnabled', "Column Selection"); this.columnSelectionModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.columnSelectionMode', "Column Selection Mode"), text, ariaLabel: text, tooltip: localize('disableColumnSelectionMode', "Disable Column Selection Mode"), command: 'editor.action.toggleColumnSelection', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.columnSelectionMode', localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); + }, 'status.editor.columnSelectionMode', StatusbarAlignment.RIGHT, 100.8); } } else { this.columnSelectionModeElement.clear(); @@ -440,12 +442,13 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { if (!this.screenRedearModeElement.value) { const text = localize('screenReaderDetected', "Screen Reader Optimized"); this.screenRedearModeElement.value = this.statusbarService.addEntry({ + name: localize('status.editor.screenReaderMode', "Screen Reader Mode"), text, ariaLabel: text, command: 'showEditorScreenReaderNotification', backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) - }, 'status.editor.screenReaderMode', localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6); + }, 'status.editor.screenReaderMode', StatusbarAlignment.RIGHT, 100.6); } } else { this.screenRedearModeElement.clear(); @@ -459,13 +462,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.selection', "Editor Selection"), text, ariaLabel: text, tooltip: localize('gotoLine', "Go to Line/Column"), command: 'workbench.action.gotoLine' }; - this.updateElement(this.selectionElement, props, 'status.editor.selection', localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5); + this.updateElement(this.selectionElement, props, 'status.editor.selection', StatusbarAlignment.RIGHT, 100.5); } private updateIndentationElement(text: string | undefined): void { @@ -475,13 +479,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.indentation', "Editor Indentation"), text, ariaLabel: text, tooltip: localize('selectIndentation', "Select Indentation"), command: 'changeEditorIndentation' }; - this.updateElement(this.indentationElement, props, 'status.editor.indentation', localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4); + this.updateElement(this.indentationElement, props, 'status.editor.indentation', StatusbarAlignment.RIGHT, 100.4); } private updateEncodingElement(text: string | undefined): void { @@ -491,13 +496,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.encoding', "Editor Encoding"), text, ariaLabel: text, tooltip: localize('selectEncoding', "Select Encoding"), command: 'workbench.action.editor.changeEncoding' }; - this.updateElement(this.encodingElement, props, 'status.editor.encoding', localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3); + this.updateElement(this.encodingElement, props, 'status.editor.encoding', StatusbarAlignment.RIGHT, 100.3); } private updateEOLElement(text: string | undefined): void { @@ -507,13 +513,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.eol', "Editor End of Line"), text, ariaLabel: text, tooltip: localize('selectEOL', "Select End of Line Sequence"), command: 'workbench.action.editor.changeEOL' }; - this.updateElement(this.eolElement, props, 'status.editor.eol', localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2); + this.updateElement(this.eolElement, props, 'status.editor.eol', StatusbarAlignment.RIGHT, 100.2); } private updateModeElement(text: string | undefined): void { @@ -523,13 +530,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.mode', "Editor Language"), text, ariaLabel: text, tooltip: localize('selectLanguageMode', "Select Language Mode"), command: 'workbench.action.editor.changeLanguageMode' }; - this.updateElement(this.modeElement, props, 'status.editor.mode', localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1); + this.updateElement(this.modeElement, props, 'status.editor.mode', StatusbarAlignment.RIGHT, 100.1); } private updateMetadataElement(text: string | undefined): void { @@ -539,17 +547,18 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } const props: IStatusbarEntry = { + name: localize('status.editor.info', "File Information"), text, ariaLabel: text, tooltip: localize('fileInfo', "File Information") }; - this.updateElement(this.metadataElement, props, 'status.editor.info', localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100); + this.updateElement(this.metadataElement, props, 'status.editor.info', StatusbarAlignment.RIGHT, 100); } - private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number) { + private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: number) { if (!element.value) { - element.value = this.statusbarService.addEntry(props, id, name, alignment, priority); + element.value = this.statusbarService.addEntry(props, id, alignment, priority); } else { element.value.update(props); } @@ -923,9 +932,9 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { const line = splitLines(this.currentMarker.message)[0]; const text = `${this.getType(this.currentMarker)} ${line}`; if (!this.statusBarEntryAccessor.value) { - this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT); + this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ name: localize('currentProblem', "Current Problem"), text: '', ariaLabel: '' }, 'statusbar.currentProblem', StatusbarAlignment.LEFT); } - this.statusBarEntryAccessor.value.update({ text, ariaLabel: text }); + this.statusBarEntryAccessor.value.update({ name: localize('currentProblem', "Current Problem"), text, ariaLabel: text }); } else { this.statusBarEntryAccessor.clear(); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index 228208bd180..063cf045525 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -71,6 +71,7 @@ export class NotificationsStatus extends Disposable { // Show the bell with a dot if there are unread or in-progress notifications const statusProperties: IStatusbarEntry = { + name: localize('status.notifications', "Notifications"), text: `${notificationsInProgress > 0 || this.newNotificationsCount > 0 ? '$(bell-dot)' : '$(bell)'}`, ariaLabel: localize('status.notifications', "Notifications"), command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, @@ -82,7 +83,6 @@ export class NotificationsStatus extends Disposable { this.notificationsCenterStatusItem = this.statusbarService.addEntry( statusProperties, 'status.notifications', - localize('status.notifications', "Notifications"), StatusbarAlignment.RIGHT, -Number.MAX_VALUE /* towards the far end of the right hand side */ ); @@ -180,9 +180,12 @@ export class NotificationsStatus extends Disposable { let statusMessageEntry: IStatusbarEntryAccessor; let showHandle: any = setTimeout(() => { statusMessageEntry = this.statusbarService.addEntry( - { text: message, ariaLabel: message }, + { + name: localize('status.message', "Status Message"), + text: message, + ariaLabel: message + }, 'status.message', - localize('status.message', "Status Message"), StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */ ); diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index a2e0dcc2ffc..aa59b862c83 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -66,7 +66,6 @@ interface IStatusbarEntryPriority { interface IPendingStatusbarEntry { id: string; - name: string; entry: IStatusbarEntry; alignment: StatusbarAlignment; priority: IStatusbarEntryPriority; @@ -445,7 +444,7 @@ export class StatusbarPart extends Part implements IStatusbarService { this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); } - addEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, primaryPriority = 0): IStatusbarEntryAccessor { + addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, primaryPriority = 0): IStatusbarEntryAccessor { const priority: IStatusbarEntryPriority = { primary: primaryPriority, secondary: hash(id) // derive from identifier to accomplish uniqueness @@ -454,15 +453,15 @@ export class StatusbarPart extends Part implements IStatusbarService { // As long as we have not been created into a container yet, record all entries // that are pending so that they can get created at a later point if (!this.element) { - return this.doAddPendingEntry(entry, id, name, alignment, priority); + return this.doAddPendingEntry(entry, id, alignment, priority); } // Otherwise add to view - return this.doAddEntry(entry, id, name, alignment, priority); + return this.doAddEntry(entry, id, alignment, priority); } - private doAddPendingEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { - const pendingEntry: IPendingStatusbarEntry = { entry, id, name, alignment, priority }; + private doAddPendingEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { + const pendingEntry: IPendingStatusbarEntry = { entry, id, alignment, priority }; this.pendingEntries.push(pendingEntry); const accessor: IStatusbarEntryAccessor = { @@ -486,7 +485,7 @@ export class StatusbarPart extends Part implements IStatusbarService { return accessor; } - private doAddEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { + private doAddEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor { // Create item const itemContainer = this.doCreateStatusItem(id, alignment, ...coalesce([entry.showBeak ? 'has-beak' : undefined])); @@ -496,7 +495,15 @@ export class StatusbarPart extends Part implements IStatusbarService { this.appendOneStatusbarEntry(itemContainer, alignment, priority); // Add to view model - const viewModelEntry: IStatusbarViewModelEntry = { id, name, alignment, priority, container: itemContainer, labelContainer: item.labelContainer }; + const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry { + readonly id = id; + readonly alignment = alignment; + readonly priority = priority; + readonly container = itemContainer; + readonly labelContainer = item.labelContainer; + + get name() { return item.name; } + }; const viewModelEntryDispose = this.viewModel.add(viewModelEntry); return { @@ -580,7 +587,7 @@ export class StatusbarPart extends Part implements IStatusbarService { while (this.pendingEntries.length) { const pending = this.pendingEntries.shift(); if (pending) { - pending.accessor = this.addEntry(pending.entry, pending.id, pending.name, pending.alignment, pending.priority.primary); + pending.accessor = this.addEntry(pending.entry, pending.id, pending.alignment, pending.priority.primary); } } } @@ -815,6 +822,7 @@ class StatusbarEntryItem extends Disposable { private readonly label: StatusBarCodiconLabel; private entry: IStatusbarEntry | undefined = undefined; + get name(): string { return assertIsDefined(this.entry).name; } private readonly foregroundListener = this._register(new MutableDisposable()); private readonly backgroundListener = this._register(new MutableDisposable()); diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index a83612abdca..91806d76519 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -23,7 +23,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { ) { const addStatusBarEntry = () => { - this.entryAccessor = this.statusBarService.addEntry(this.entry, 'status.debug', nls.localize('status.debug', "Debug"), StatusbarAlignment.LEFT, 30 /* Low Priority */); + this.entryAccessor = this.statusBarService.addEntry(this.entry, 'status.debug', StatusbarAlignment.LEFT, 30 /* Low Priority */); }; const setShowInStatusBar = () => { @@ -65,6 +65,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { } return { + name: nls.localize('status.debug', "Debug"), text: '$(debug-alt-small) ' + text, ariaLabel: nls.localize('debugTarget', "Debug: {0}", text), tooltip: nls.localize('selectAndStartDebug', "Select and start debug configuration"), diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index e93f8f483df..ac64051f0aa 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -82,6 +82,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio if (visible) { const indicator: IStatusbarEntry = { + name: nls.localize('status.profiler', "Extension Profiler"), text: nls.localize('profilingExtensionHost', "Profiling Extension Host"), showProgress: true, ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"), @@ -98,7 +99,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle)); if (!this.profilingStatusBarIndicator) { - this.profilingStatusBarIndicator = this._statusbarService.addEntry(indicator, 'status.profiler', nls.localize('status.profiler', "Extension Profiler"), StatusbarAlignment.RIGHT); + this.profilingStatusBarIndicator = this._statusbarService.addEntry(indicator, 'status.profiler', StatusbarAlignment.RIGHT); } else { this.profilingStatusBarIndicator.update(indicator); } diff --git a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts index 07b502f176f..7708f4a8f65 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts @@ -80,7 +80,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben private createFeedbackStatusEntry(): void { // Status entry - this.entry = this._register(this.statusbarService.addEntry(this.getStatusEntry(), 'status.feedback', localize('status.feedback', "Tweet Feedback"), StatusbarAlignment.RIGHT, -100 /* towards the end of the right hand side */)); + this.entry = this._register(this.statusbarService.addEntry(this.getStatusEntry(), 'status.feedback', StatusbarAlignment.RIGHT, -100 /* towards the end of the right hand side */)); // Command to toggle CommandsRegistry.registerCommand(FeedbackStatusbarConribution.TOGGLE_FEEDBACK_COMMAND, () => this.toggleFeedback()); @@ -136,6 +136,7 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben private getStatusEntry(showBeak?: boolean): IStatusbarEntry { return { + name: localize('status.feedback.name', "Feedback"), text: '$(feedback)', ariaLabel: localize('status.feedback', "Tweet Feedback"), tooltip: localize('status.feedback', "Tweet Feedback"), diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index e0d049f9a56..14b184c6cdb 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -374,7 +374,7 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont @IStatusbarService private readonly statusbarService: IStatusbarService ) { super(); - this.markersStatusItem = this._register(this.statusbarService.addEntry(this.getMarkersItem(), 'status.problems', localize('status.problems', "Problems"), StatusbarAlignment.LEFT, 50 /* Medium Priority */)); + this.markersStatusItem = this._register(this.statusbarService.addEntry(this.getMarkersItem(), 'status.problems', StatusbarAlignment.LEFT, 50 /* Medium Priority */)); this.markerService.onMarkerChanged(() => this.markersStatusItem.update(this.getMarkersItem())); } @@ -382,6 +382,7 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont const markersStatistics = this.markerService.getStatistics(); const tooltip = this.getMarkersTooltip(markersStatistics); return { + name: localize('status.problems', "Problems"), text: this.getMarkersText(markersStatistics), ariaLabel: tooltip, tooltip, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index f06c862be27..9538da6465c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -282,13 +282,13 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { const tooltip = kernel.description ?? kernel.detail ?? kernel.label; this._kernelInfoElement.add(this._statusbarService.addEntry( { + name: nls.localize('notebook.info', "Notebook Kernel Info"), text: `$(notebook-kernel-select) ${kernel.label}`, ariaLabel: kernel.label, tooltip: isSuggested ? nls.localize('tooltop', "{0} (suggestion)", tooltip) : tooltip, command: SELECT_KERNEL_ID, }, 'notebook.selectKernel', - nls.localize('notebook.info', "Notebook Kernel Info"), StatusbarAlignment.RIGHT, 10 )); @@ -300,13 +300,13 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { // multiple kernels -> show selection hint this._kernelInfoElement.add(this._statusbarService.addEntry( { + name: nls.localize('notebook.select', "Notebook Kernel Selection"), text: nls.localize('kernel.select.label', "Select Kernel"), ariaLabel: nls.localize('kernel.select.label', "Select Kernel"), command: SELECT_KERNEL_ID, backgroundColor: { id: 'statusBarItem.prominentBackground' } }, 'notebook.selectKernel', - nls.localize('notebook.select', "Notebook Kernel Selection"), StatusbarAlignment.RIGHT, 10 )); @@ -354,12 +354,11 @@ export class ActiveCellStatus extends Disposable implements IWorkbenchContributi return; } - const entry = { text: newText, ariaLabel: newText }; + const entry = { name: nls.localize('notebook.activeCellStatusName', "Notebook Editor Selections"), text: newText, ariaLabel: newText }; if (!this._accessor.value) { this._accessor.value = this._statusbarService.addEntry( entry, 'notebook.activeCellStatus', - nls.localize('notebook.activeCellStatusName', "Notebook Editor Selections"), StatusbarAlignment.RIGHT, 100 ); diff --git a/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts b/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts index d3a90dcfcc2..9b789932749 100644 --- a/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts +++ b/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts @@ -32,6 +32,8 @@ export class KeyboardLayoutPickerContribution extends Disposable implements IWor ) { super(); + const name = nls.localize('status.workbench.keyboardLayout', "Keyboard Layout"); + let layout = this.keyboardLayoutService.getCurrentKeyboardLayout(); if (layout) { let layoutInfo = parseKeyboardLayoutDescription(layout); @@ -39,12 +41,12 @@ export class KeyboardLayoutPickerContribution extends Disposable implements IWor this.pickerElement.value = this.statusbarService.addEntry( { + name, text, ariaLabel: text, command: KEYBOARD_LAYOUT_OPEN_PICKER }, 'status.workbench.keyboardLayout', - nls.localize('status.workbench.keyboardLayout', "Keyboard Layout"), StatusbarAlignment.RIGHT ); } @@ -56,6 +58,7 @@ export class KeyboardLayoutPickerContribution extends Disposable implements IWor if (this.pickerElement.value) { const text = nls.localize('keyboardLayout', "Layout: {0}", layoutInfo.label); this.pickerElement.value.update({ + name, text, ariaLabel: text, command: KEYBOARD_LAYOUT_OPEN_PICKER @@ -64,12 +67,12 @@ export class KeyboardLayoutPickerContribution extends Disposable implements IWor const text = nls.localize('keyboardLayout', "Layout: {0}", layoutInfo.label); this.pickerElement.value = this.statusbarService.addEntry( { + name, text, ariaLabel: text, command: KEYBOARD_LAYOUT_OPEN_PICKER }, 'status.workbench.keyboardLayout', - nls.localize('status.workbench.keyboardLayout', "Keyboard Layout"), StatusbarAlignment.RIGHT ); } diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index b437ae61a92..33a5e367bc0 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -123,7 +123,7 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu private updateStatusBar() { if (!this.entryAccessor) { - this._register(this.entryAccessor = this.statusbarService.addEntry(this.entry, 'status.forwardedPorts', nls.localize('status.forwardedPorts', "Forwarded Ports"), StatusbarAlignment.LEFT, 40)); + this._register(this.entryAccessor = this.statusbarService.addEntry(this.entry, 'status.forwardedPorts', StatusbarAlignment.LEFT, 40)); } else { this.entryAccessor.update(this.entry); } @@ -143,6 +143,7 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu allTunnels.map(forwarded => forwarded.remotePort).join(', ')); } return { + name: nls.localize('status.forwardedPorts', "Forwarded Ports"), text: `$(radio-tower) ${text}`, ariaLabel: tooltip, tooltip, diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 99ba6ad9d14..342ce7934eb 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -317,6 +317,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr const ariaLabel = getCodiconAriaLabel(text); const properties: IStatusbarEntry = { + name, backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), ariaLabel, @@ -329,7 +330,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr if (this.remoteStatusEntry) { this.remoteStatusEntry.update(properties); } else { - this.remoteStatusEntry = this.statusbarService.addEntry(properties, 'status.host', name, StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); + this.remoteStatusEntry = this.statusbarService.addEntry(properties, 'status.host', StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); } } diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index fcfe819b884..8e4de79194b 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -154,11 +154,12 @@ export class SCMStatusController implements IWorkbenchContribution { ariaLabel = ariaLabel ? `${ariaLabel}, ${label}` : label; disposables.add(this.statusbarService.addEntry({ + name: localize('status.scm', "Source Control"), text: command.title, ariaLabel: `${ariaLabel}${command.tooltip ? ` - ${command.tooltip}` : ''}`, tooltip, command: command.id ? command : undefined - }, 'status.scm', localize('status.scm', "Source Control"), MainThreadStatusBarAlignment.LEFT, 10000)); + }, 'status.scm', MainThreadStatusBarAlignment.LEFT, 10000)); } this.statusBarDisposable = disposables; diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index e1f1eb3aa5f..b3bceff3270 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -130,6 +130,7 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench } } else { const itemProps: IStatusbarEntry = { + name: nls.localize('status.runningTasks', "Running Tasks"), text: `$(tools) ${tasks.length}`, ariaLabel: nls.localize('numberOfRunningTasks', "{0} running tasks", tasks.length), tooltip: nls.localize('runningTasks', "Show Running Tasks"), @@ -137,7 +138,7 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench }; if (!this.runningTasksStatusItem) { - this.runningTasksStatusItem = this.statusbarService.addEntry(itemProps, 'status.runningTasks', nls.localize('status.runningTasks', "Running Tasks"), StatusbarAlignment.LEFT, 49 /* Medium Priority, next to Markers */); + this.runningTasksStatusItem = this.statusbarService.addEntry(itemProps, 'status.runningTasks', StatusbarAlignment.LEFT, 49 /* Medium Priority, next to Markers */); } else { this.runningTasksStatusItem.update(itemProps); } diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 902e74d93d5..4abb2b498c6 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -194,7 +194,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben private createStatusbarEntry(): void { const entry = this.getStatusbarEntry(this.workspaceTrustManagementService.isWorkpaceTrusted()); - this.statusbarEntryAccessor.value = this.statusbarService.addEntry(entry, this.entryId, localize('status.WorkspaceTrust', "Workspace Trust"), StatusbarAlignment.LEFT, 0.99 * Number.MAX_VALUE /* Right of remote indicator */); + this.statusbarEntryAccessor.value = this.statusbarService.addEntry(entry, this.entryId, StatusbarAlignment.LEFT, 0.99 * Number.MAX_VALUE /* Right of remote indicator */); this.statusbarService.updateEntryVisibility(this.entryId, false); } @@ -224,6 +224,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben const color = new ThemeColor(STATUS_BAR_PROMINENT_ITEM_FOREGROUND); return { + name: localize('status.WorkspaceTrust', "Workspace Trust"), text: trusted ? `$(shield)` : `$(shield) ${text}`, ariaLabel: trusted ? localize('status.ariaTrusted', "This workspace is trusted.") : localize('status.ariaUntrusted', "Restricted Mode: Some features are disabled because this workspace is not trusted."), tooltip: trusted ? localize('status.tooltipTrusted', "This workspace is trusted.") : localize('status.tooltipUntrusted', "Some features are disabled because this workspace is not trusted."), diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 82b7a7dfc45..fbdb00a85f6 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -152,6 +152,7 @@ export class ProgressService extends Disposable implements IProgressService { } const statusEntryProperties: IStatusbarEntry = { + name: localize('status.progress', "Progress Message"), text, showProgress: true, ariaLabel: text, @@ -162,7 +163,7 @@ export class ProgressService extends Disposable implements IProgressService { if (this.windowProgressStatusEntry) { this.windowProgressStatusEntry.update(statusEntryProperties); } else { - this.windowProgressStatusEntry = this.statusbarService.addEntry(statusEntryProperties, 'status.progress', localize('status.progress', "Progress Message"), StatusbarAlignment.LEFT); + this.windowProgressStatusEntry = this.statusbarService.addEntry(statusEntryProperties, 'status.progress', StatusbarAlignment.LEFT); } } diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index ee19a350567..7ef7ee73e64 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -21,6 +21,12 @@ export const enum StatusbarAlignment { */ export interface IStatusbarEntry { + /** + * The (short) name to show for the entry like 'Language Indicator', + * 'Git Status' etc. + */ + readonly name: string; + /** * The text to show for the entry. You can embed icons in the text by leveraging the syntax: * @@ -79,12 +85,11 @@ export interface IStatusbarService { * to update or remove the statusbar entry. * * @param id identifier of the entry is needed to allow users to hide entries via settings - * @param name human readable name the entry is about * @param alignment either LEFT or RIGHT * @param priority items get arranged from highest priority to lowest priority from left to right * in their respective alignment slot */ - addEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority?: number): IStatusbarEntryAccessor; + addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority?: number): IStatusbarEntryAccessor; /** * An event that is triggered when an entry's visibility is changed. From faa3146a31007232d3af3bc2b086b1e8b8332884 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 05:53:57 -0700 Subject: [PATCH 107/270] Add ending period fto prof-startup --- src/vs/platform/environment/node/argv.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 1b5db388b7f..8988ac3dfd6 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -63,7 +63,7 @@ export const OPTIONS: OptionDescriptions> = { 'verbose': { type: 'boolean', cat: 't', description: localize('verbose', "Print verbose output (implies --wait).") }, 'log': { type: 'string', cat: 't', args: 'level', description: localize('log', "Log level to use. Default is 'info'. Allowed values are 'critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'.") }, 'status': { type: 'boolean', alias: 's', cat: 't', description: localize('status', "Print process usage and diagnostics information.") }, - 'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup") }, + 'prof-startup': { type: 'boolean', cat: 't', description: localize('prof-startup', "Run CPU profiler during startup.") }, 'prof-append-timers': { type: 'string' }, 'prof-startup-prefix': { type: 'string' }, 'prof-v8-extensions': { type: 'boolean' }, From a0819ed4fea90aa88f4b9838ff26da3251ba6445 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 05:55:52 -0700 Subject: [PATCH 108/270] Add ending period to --sync --- src/vs/platform/environment/node/argv.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 8988ac3dfd6..779b2e1286c 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -69,7 +69,7 @@ export const OPTIONS: OptionDescriptions> = { 'prof-v8-extensions': { type: 'boolean' }, 'disable-extensions': { type: 'boolean', deprecates: 'disableExtensions', cat: 't', description: localize('disableExtensions', "Disable all installed extensions.") }, 'disable-extension': { type: 'string[]', cat: 't', args: 'extension-id', description: localize('disableExtension', "Disable an extension.") }, - 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off"), args: ['on', 'off'] }, + 'sync': { type: 'string', cat: 't', description: localize('turn sync', "Turn sync on or off."), args: ['on', 'off'] }, 'inspect-extensions': { type: 'string', deprecates: 'debugPluginHost', args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") }, 'inspect-brk-extensions': { type: 'string', deprecates: 'debugBrkPluginHost', args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") }, From b7058688ad38dd0f4ba1f14ead629dfcc65ae654 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 19 May 2021 15:01:51 +0200 Subject: [PATCH 109/270] Fixes #124143 --- src/vs/workbench/browser/layout.ts | 7 ++++++- src/vs/workbench/browser/parts/banner/bannerPart.ts | 1 + src/vs/workbench/services/layout/browser/layoutService.ts | 5 +++++ src/vs/workbench/test/browser/workbenchTestServices.ts | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index e199ec8a9b0..651f3cf61f5 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1366,6 +1366,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.setViewVisible(this.activityBarPartView, !hidden); } + setBannerHidden(hidden: boolean): void { + this.workbenchGrid.setViewVisible(this.bannerPartView, !hidden); + } + setEditorHidden(hidden: boolean, skipLayout?: boolean): void { this.state.editor.hidden = hidden; @@ -1806,7 +1810,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi { type: 'leaf', data: { type: Parts.BANNER_PART }, - size: bannerHeight + size: bannerHeight, + visible: false }, { type: 'branch', diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 4a042ca9d19..a894eafc722 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -178,6 +178,7 @@ export class BannerPart extends Part implements IBannerService { this.visible = visible; this.focusedActionIndex = -1; + this.layoutService.setBannerHidden(!visible); this._onDidChangeSize.fire(undefined); } } diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 25edebb2c71..83d72d6341e 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -163,6 +163,11 @@ export interface IWorkbenchLayoutService extends ILayoutService { */ setActivityBarHidden(hidden: boolean): void; + /** + * Set banner hidden or not + */ + setBannerHidden(hidden: boolean): void; + /** * * Set editor area hidden or not diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 6ea06def96d..2414d2220b9 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -541,6 +541,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { isStatusBarHidden(): boolean { return false; } isActivityBarHidden(): boolean { return false; } setActivityBarHidden(_hidden: boolean): void { } + setBannerHidden(_hidden: boolean): void { } isSideBarHidden(): boolean { return false; } async setEditorHidden(_hidden: boolean): Promise { } async setSideBarHidden(_hidden: boolean): Promise { } From 53352a295408aacce8994d35ebe587a9676cdf7f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 19 May 2021 15:27:23 +0200 Subject: [PATCH 110/270] add API to open an untitled notebook, https://github.com/microsoft/vscode/issues/121974 --- .../notebook.document.test.ts | 24 +++++++ .../src/singlefolder-tests/notebook.test.ts | 4 +- src/vs/vscode.proposed.d.ts | 11 ++++ .../browser/mainThreadNotebookDocuments.ts | 32 ++++++++-- .../workbench/api/common/extHost.api.impl.ts | 13 +++- .../workbench/api/common/extHost.protocol.ts | 5 +- .../workbench/api/common/extHostNotebook.ts | 28 ++++----- .../api/common/extHostNotebookDocument.ts | 2 +- .../api/common/extHostTypeConverters.ts | 22 +++++++ .../notebook/common/notebookEditorModel.ts | 62 +++++++++++++------ .../notebookEditorModelResolverServiceImpl.ts | 12 ++-- 11 files changed, 164 insertions(+), 51 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index cf4e029e497..0739e4e8afc 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -140,6 +140,30 @@ suite('Notebook Document', function () { await p; }); + test('open untitled notebook', async function () { + const nb = await vscode.notebook.openNotebookDocument('notebook.nbdserializer'); + assert.strictEqual(nb.isUntitled, true); + assert.strictEqual(nb.isClosed, false); + assert.strictEqual(nb.uri.scheme, 'untitled'); + // assert.strictEqual(nb.cellCount, 0); // NotebookSerializer ALWAYS returns something here + }); + + test('open untitled with data', async function () { + const nb = await vscode.notebook.openNotebookDocument( + 'notebook.nbdserializer', + new vscode.NotebookData([ + new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'console.log()', 'javascript'), + new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, 'Hey', 'markdown'), + ]) + ); + assert.strictEqual(nb.isUntitled, true); + assert.strictEqual(nb.isClosed, false); + assert.strictEqual(nb.uri.scheme, 'untitled'); + assert.strictEqual(nb.cellCount, 2); + assert.strictEqual(nb.cellAt(0).kind, vscode.NotebookCellKind.Code); + assert.strictEqual(nb.cellAt(1).kind, vscode.NotebookCellKind.Markup); + }); + test('workspace edit API (replaceCells)', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index ca3456f934f..b00302b2808 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -104,8 +104,8 @@ suite('Notebook API tests', function () { suiteSetup(function () { suiteDisposables.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { - openNotebook: async (_resource: vscode.Uri): Promise => { - if (/.*empty\-.*\.vsctestnb$/.test(_resource.path)) { + openNotebook: async (resource: vscode.Uri): Promise => { + if (/.*empty\-.*\.vsctestnb$/.test(resource.path)) { return { metadata: new vscode.NotebookDocumentMetadata(), cells: [] diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 147fcfb5ad6..16b6b7e85fd 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1722,6 +1722,17 @@ declare module 'vscode' { */ export function openNotebookDocument(uri: Uri): Thenable; + /** + * Open an untitled notebook. The editor will prompt the user for a file + * path when the document is to be saved. + * + * @see {@link openNotebookDocument} + * @param viewType The notebook view type that should be used. + * @param content The initial contents of the notebook. + * @returns A promise that resolves to a {@link NotebookDocument notebook}. + */ + export function openNotebookDocument(viewType: string, content?: NotebookData): Thenable; + /** * An event that is emitted when a {@link NotebookDocument notebook} is opened. */ diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index 3d2785cd483..bad31cf5a11 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -9,13 +9,14 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { IImmediateCellEditOperation, IMainCellDto, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IImmediateCellEditOperation, IMainCellDto, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, MainThreadNotebookDocumentsShape } from '../common/extHost.protocol'; import { MainThreadNotebooksAndEditors } from 'vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { Schemas } from 'vs/base/common/network'; export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsShape { @@ -47,7 +48,6 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS this._disposables.dispose(); this._modelReferenceCollection.dispose(); dispose(this._documentEventListenersMapping.values()); - } private _handleNotebooksAdded(notebooks: readonly NotebookTextModel[]): void { @@ -119,14 +119,38 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS }; } - async $tryOpenDocument(uriComponents: UriComponents): Promise { + async $tryCreateNotebook(options: { viewType?: string, content?: NotebookDataDto }): Promise { + + // find a free URI for the untitled case + let uri: URI; + for (let counter = 1; ; counter++) { + let candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-Notebook-${counter}` }); + if (!this._notebookService.getNotebookTextModel(candidate)) { + uri = candidate; + break; + } + } + + const ref = await this._notebookEditorModelResolverService.resolve(uri, options.viewType); + this._modelReferenceCollection.add(ref.object.resource, ref); + if (options.content) { + ref.object.notebook.reset( + options.content.cells, + options.content.metadata, + ref.object.notebook.transientOptions + ); + } + return uri; + } + + async $tryOpenNotebook(uriComponents: UriComponents): Promise { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri, undefined); this._modelReferenceCollection.add(uri, ref); return uri; } - async $trySaveDocument(uriComponents: UriComponents) { + async $trySaveNotebook(uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e0f69afce4c..71b0809eb0f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1036,9 +1036,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: notebook const notebook: typeof vscode.notebook = { - openNotebookDocument: (uriComponents) => { + async openNotebookDocument(uriOrOptions?: URI | string, content?: vscode.NotebookData) { checkProposedApiEnabled(extension); - return extHostNotebook.openNotebookDocument(uriComponents); + let uri: URI; + if (URI.isUri(uriOrOptions)) { + uri = uriOrOptions; + await extHostNotebook.openNotebookDocument(uriOrOptions); + } else if (typeof uriOrOptions === 'string') { + uri = URI.revive(await extHostNotebook.createNotebookDocument({ viewType: uriOrOptions, content })); + } else { + throw new Error('Invalid arguments'); + } + return extHostNotebook.getNotebookDocument(uri).apiNotebook; }, get onDidOpenNotebookDocument(): Event { checkProposedApiEnabled(extension); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index aee0a700928..3b12c7d8a37 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -888,8 +888,9 @@ export interface MainThreadNotebookEditorsShape extends IDisposable { } export interface MainThreadNotebookDocumentsShape extends IDisposable { - $tryOpenDocument(uriComponents: UriComponents): Promise; - $trySaveDocument(uri: UriComponents): Promise; + $tryCreateNotebook(options: { viewType?: string, content?: NotebookDataDto }): Promise; + $tryOpenNotebook(uriComponents: UriComponents): Promise; + $trySaveNotebook(uri: UriComponents): Promise; $applyEdits(resource: UriComponents, edits: IImmediateCellEditOperation[], computeUndoRedo?: boolean): Promise; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index e7956730308..d505510df63 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -254,12 +254,20 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return new NotebookEditorDecorationType(this._notebookEditorsProxy, options).value; } + async createNotebookDocument(options: { viewType?: string, content?: vscode.NotebookData } = {}): Promise { + const canonicalUri = await this._notebookDocumentsProxy.$tryCreateNotebook({ + viewType: options.viewType, + content: options.content && typeConverters.NotebookData.from(options.content) + }); + return URI.revive(canonicalUri); + } + async openNotebookDocument(uri: URI): Promise { const cached = this._documents.get(uri); if (cached) { return cached.apiNotebook; } - const canonicalUri = await this._notebookDocumentsProxy.$tryOpenDocument(uri); + const canonicalUri = await this._notebookDocumentsProxy.$tryOpenNotebook(uri); const document = this._documents.get(URI.revive(canonicalUri)); return assertIsDefined(document?.apiNotebook); } @@ -358,19 +366,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (!serializer) { throw new Error('NO serializer found'); } - const data = await serializer.deserializeNotebook(bytes.buffer, token); - const res: NotebookDataDto = { - metadata: typeConverters.NotebookDocumentMetadata.from(data.metadata), - cells: [], - }; - - for (let cell of data.cells) { - extHostTypes.NotebookCellData.validate(cell); - res.cells.push(typeConverters.NotebookCellData.from(cell)); - } - - return res; + return typeConverters.NotebookData.from(data); } async $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise { @@ -378,10 +375,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (!serializer) { throw new Error('NO serializer found'); } - const bytes = await serializer.serializeNotebook({ - metadata: typeConverters.NotebookDocumentMetadata.to(data.metadata), - cells: data.cells.map(typeConverters.NotebookCellData.to) - }, token); + const bytes = await serializer.serializeNotebook(typeConverters.NotebookData.to(data), token); return VSBuffer.wrap(bytes); } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 131569c1f74..7fd979ad543 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -258,7 +258,7 @@ export class ExtHostNotebookDocument { if (this._disposed) { return Promise.reject(new Error('Notebook has been closed')); } - return this._proxy.$trySaveDocument(this.uri); + return this._proxy.$trySaveNotebook(this.uri); } private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 72eccc15c02..9aaec755c6b 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1485,6 +1485,28 @@ export namespace NotebookCellKind { } } +export namespace NotebookData { + + export function from(data: vscode.NotebookData): notebooks.NotebookDataDto { + const res: notebooks.NotebookDataDto = { + metadata: NotebookDocumentMetadata.from(data.metadata), + cells: [], + }; + for (let cell of data.cells) { + types.NotebookCellData.validate(cell); + res.cells.push(NotebookCellData.from(cell)); + } + return res; + } + + export function to(data: notebooks.NotebookDataDto): vscode.NotebookData { + return { + metadata: NotebookDocumentMetadata.to(data.metadata), + cells: data.cells.map(NotebookCellData.to) + }; + } +} + export namespace NotebookCellData { export function from(data: vscode.NotebookCellData): notebooks.ICellDto2 { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 03cb38b4b63..5fc06a8afdb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -28,8 +28,9 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { filter } from 'vs/base/common/objects'; +import { IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; +import { IResolvedUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; //#region --- complex content provider @@ -425,13 +426,13 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE readonly onDidChangeOrphaned: Event = this._onDidChangeOrphaned.event; readonly onDidChangeReadonly: Event = this._onDidChangeReadonly.event; - private _workingCopy?: IResolvedFileWorkingCopy; + private _workingCopy?: IResolvedUntitledFileWorkingCopy | IResolvedFileWorkingCopy; private readonly _workingCopyListeners = new DisposableStore(); constructor( readonly resource: URI, readonly viewType: string, - private readonly _workingCopyManager: IFileWorkingCopyManager, + private readonly _workingCopyManager: IFileWorkingCopyManager2, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IFileService private readonly _fileService: IFileService ) { @@ -461,11 +462,17 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } isOrphaned(): boolean { - return this._workingCopy?.hasState(FileWorkingCopyState.ORPHAN) ?? false; + return !!this._workingCopy && SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(FileWorkingCopyState.ORPHAN); } isReadonly(): boolean { - return this._workingCopy?.isReadonly() || this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + if (this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + return true; + } else if (this._workingCopy && SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy)) { + return this._workingCopy?.isReadonly(); + } else { + return false; + } } revert(options?: IRevertOptions): Promise { @@ -479,14 +486,26 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } async load(options?: INotebookLoadOptions): Promise { - const workingCopy = await this._workingCopyManager.resolve(this.resource, { reload: { async: !options?.forceReadFromFile } }); + if (!this._workingCopy) { - this._workingCopy = >workingCopy; - this._workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(), this._workingCopyListeners); - this._workingCopy.onDidSave(() => this._onDidSave.fire(), this._workingCopyListeners); - this._workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire(), this._workingCopyListeners); - this._workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire(), this._workingCopyListeners); + if (this.resource.scheme === Schemas.untitled) { + const workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource }); + this._workingCopy = >workingCopy; + } else { + const workingCopy = await this._workingCopyManager.resolve(this.resource, { forceReadFromFile: options?.forceReadFromFile }); + this._workingCopyListeners.add(workingCopy.onDidSave(() => this._onDidSave.fire())); + this._workingCopyListeners.add(workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); + this._workingCopyListeners.add(workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire())); + this._workingCopy = >workingCopy; + } + this._workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(), undefined, this._workingCopyListeners); + + this._workingCopyListeners.add(this._workingCopy.onWillDispose(() => { + this._workingCopyListeners.clear(); + this._workingCopy?.model.dispose(); + })); } + assertType(this.isResolved()); return this; } @@ -501,11 +520,15 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE // the newly created editor input will pick it up and claim ownership of it. return this._instantiationService.createInstance(NotebookEditorInput, newWorkingCopy.resource, this.viewType, {}); } + + private static _isFileWorkingCopy(candidate: IResolvedUntitledFileWorkingCopy | IResolvedFileWorkingCopy): candidate is IResolvedFileWorkingCopy { + return typeof (>candidate).hasState === 'function'; + } } -export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel { +export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel, IUntitledFileWorkingCopyModel { - private readonly _onDidChangeContent = new Emitter(); + private readonly _onDidChangeContent = new Emitter(); private readonly _changeListener: IDisposable; readonly onDidChangeContent = this._onDidChangeContent.event; @@ -525,10 +548,10 @@ export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel { if (rawEvent.transient) { continue; } - //todo@jrieken,@rebornix forward this information from notebook model this._onDidChangeContent.fire({ - isRedoing: false, - isUndoing: false + isRedoing: false, //todo@rebornix forward this information from notebook model + isUndoing: false, + isEmpty: false, //_notebookModel.cells.length === 0 // todo@jrieken non transient metadata? }); break; } @@ -585,7 +608,9 @@ export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel { this._notebookModel.reset(data.cells, data.metadata, this._notebookSerializer.options); } - get versionId() { return this._notebookModel.alternativeVersionId; } + get versionId() { + return this._notebookModel.alternativeVersionId; + } pushStackElement(): void { this._notebookModel.pushStackElement('save', undefined, undefined); @@ -606,7 +631,8 @@ export class NotebookFileWorkingCopyModelFactory implements IFileWorkingCopyMode throw new Error('CANNOT open file notebook with this provider'); } - const data = await info.serializer.dataToNotebook(await streamToBuffer(stream)); + const bytes = await streamToBuffer(stream); + const data = await info.serializer.dataToNotebook(bytes); if (token.isCancellationRequested) { throw canceled(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index b4dcb5c68c9..75f9ce0c647 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -11,16 +11,16 @@ import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, import { ComplexNotebookProviderInfo, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; -import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceMap } from 'vs/base/common/map'; +import { FileWorkingCopyManager2, IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; class NotebookModelReferenceCollection extends ReferenceCollection> { private readonly _disposables = new DisposableStore(); - private readonly _workingCopyManagers = new Map>(); + private readonly _workingCopyManagers = new Map>(); private readonly _modelListener = new Map(); private readonly _onDidSaveNotebook = new Emitter(); @@ -70,10 +70,12 @@ class NotebookModelReferenceCollection extends ReferenceCollection>this._instantiationService.createInstance( - FileWorkingCopyManager, + const factory = new NotebookFileWorkingCopyModelFactory(viewType, this._notebookService); + workingCopyManager = >this._instantiationService.createInstance( + FileWorkingCopyManager2, workingCopyTypeId, - new NotebookFileWorkingCopyModelFactory(viewType, this._notebookService) + factory, + factory, ); this._workingCopyManagers.set(workingCopyTypeId, workingCopyManager); } From 37a11814295fe9e90542d8990c30b83bb2de42fd Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 19 May 2021 18:58:20 +0530 Subject: [PATCH 111/270] do not include non configured deprecated restricted settings --- .../workspace/browser/workspaceTrustEditor.ts | 32 +++++++++++++++++-- .../configuration/common/configuration.ts | 11 +------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 08da9da034b..54b4bc06fba 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -22,6 +22,7 @@ import { Schemas } from 'vs/base/common/network'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionUntrustedWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; @@ -31,6 +32,7 @@ import { WorkbenchTable } from 'vs/platform/list/browser/listService'; import { IPromptChoiceWithMenu } from 'vs/platform/notification/common/notification'; import { Link } from 'vs/platform/opener/browser/link'; import product from 'vs/platform/product/common/product'; +import { Registry } from 'vs/platform/registry/common/platform'; import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -47,7 +49,7 @@ import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/deb import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { getInstalledExtensions, IExtensionStatus } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; import { settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; -import { filterSettingsRequireWorkspaceTrust, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; @@ -659,7 +661,33 @@ export class WorkspaceTrustEditor extends EditorPane { this.rootElement.setAttribute('aria-label', `${localize('root element label', "Manage Workspace Trust")}: ${this.headerContainer.innerText}`); // Settings - const settingsRequiringTrustedWorkspaceCount = filterSettingsRequireWorkspaceTrust(this.configurationService.restrictedSettings.default).length; + const restrictedSettings = this.configurationService.restrictedSettings; + const configurationRegistry = Registry.as(Extensions.Configuration); + const settingsRequiringTrustedWorkspaceCount = restrictedSettings.default.filter(key => { + const property = configurationRegistry.getConfigurationProperties()[key]; + + // cannot be configured in workspace + if (property.scope === ConfigurationScope.APPLICATION || property.scope === ConfigurationScope.MACHINE) { + return false; + } + + // If deprecated include only those configured in the workspace + if (property.deprecationMessage || property.markdownDeprecationMessage) { + if (restrictedSettings.workspace?.includes(key)) { + return true; + } + if (restrictedSettings.workspaceFolder) { + for (const workspaceFolderSettings of restrictedSettings.workspaceFolder.values()) { + if (workspaceFolderSettings.includes(key)) { + return true; + } + } + } + return false; + } + + return true; + }).length; // Features List const installedExtensions = await this.instantiationService.invokeFunction(getInstalledExtensions); diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 9cff28447ca..105b65223d7 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; -import { Registry } from 'vs/platform/registry/common/platform'; export const FOLDER_CONFIG_FOLDER_NAME = '.vscode'; export const FOLDER_SETTINGS_NAME = 'settings'; @@ -48,14 +47,6 @@ export interface IConfigurationCache { } -export function filterSettingsRequireWorkspaceTrust(settings: ReadonlyArray): ReadonlyArray { - const configurationRegistry = Registry.as(Extensions.Configuration); - return settings.filter(key => { - const property = configurationRegistry.getConfigurationProperties()[key]; - return property.restricted && property.scope !== ConfigurationScope.APPLICATION && property.scope !== ConfigurationScope.MACHINE; - }); -} - export type RestrictedSettings = { default: ReadonlyArray; userLocal?: ReadonlyArray; From 53b58c89d44deb230dc526605561a1d42f6d1c50 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 19 May 2021 15:41:42 +0200 Subject: [PATCH 112/270] Fix loop in port forwarding --- .../workbench/services/remote/common/remoteExplorerService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 51210735438..43e7c540559 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -585,7 +585,7 @@ export class TunnelModel extends Disposable { existingTunnel.name = attributes?.label ?? name; this._onForwardPort.fire(); } - if (attributes?.protocol !== existingTunnel.localUri.scheme) { + if (attributes?.protocol && attributes.protocol !== existingTunnel.localUri.scheme) { await this.close(existingTunnel.remoteHost, existingTunnel.remotePort); await this.forward({ host: existingTunnel.remoteHost, port: existingTunnel.remotePort }, local, name, source, elevateIfNeeded, isPublic, restore, attributes); } From eb5874237ebfd080c84aef5af037943aa4e1498b Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 19 May 2021 16:04:28 +0200 Subject: [PATCH 113/270] Improve task terminal status message Fixes #124062 --- .../workbench/contrib/tasks/browser/taskTerminalStatus.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts index b491ab10216..05e62f5dd7a 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts @@ -22,7 +22,9 @@ interface TerminalData { const TASK_TERMINAL_STATUS_ID = 'task_terminal_status'; const ACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.play, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.active', "Task is running") }; const SUCCEEDED_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.check, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.succeeded', "Task succeeded") }; +const SUCCEEDED_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.check, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.succeededInactive', "Task succeeded and waiting...") }; const FAILED_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.error, severity: Severity.Error, tooltip: nls.localize('taskTerminalStatus.errors', "Task has errors") }; +const FAILED_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.error, severity: Severity.Error, tooltip: nls.localize('taskTerminalStatus.errorsInactive', "Task has errors and is waiting...") }; export class TaskTerminalStatus extends Disposable { private terminalMap: Map = new Map(); @@ -76,9 +78,9 @@ export class TaskTerminalStatus extends Disposable { } terminalData.terminal.statusList.remove(terminalData.status); if (terminalData.problemMatcher.numberOfMatches === 0) { - terminalData.terminal.statusList.add(SUCCEEDED_TASK_STATUS); + terminalData.terminal.statusList.add(SUCCEEDED_INACTIVE_TASK_STATUS); } else { - terminalData.terminal.statusList.add(FAILED_TASK_STATUS); + terminalData.terminal.statusList.add(FAILED_INACTIVE_TASK_STATUS); } } From 88d9a3aaf5c1d3c9b618c5e3d34b3af67f2facba Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 19 May 2021 16:33:30 +0200 Subject: [PATCH 114/270] Support command links in trusted tree hovers Fixes #123723 --- src/vs/workbench/browser/parts/views/treeView.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 29cb607489a..8e906203cc8 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -859,12 +859,18 @@ class TreeRenderer extends Disposable implements ITreeRenderer { - return this.hoverService.showHover(options); + return this.hoverService.showHover({ + ...options, + linkHandler: (url: string) => { + return openerService.open(url, { allowCommands: (!isString(options.text) && options.text.isTrusted) }); + } + }); }, delay: this.configurationService.getValue('workbench.hover.delay') }; From ff6d8771c947366e06d41a6ac3ceec56adac3237 Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 19 May 2021 16:45:44 +0200 Subject: [PATCH 115/270] fixes #124085 --- src/vs/workbench/contrib/debug/browser/debugCommands.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 05a3439914b..c862f63c9cc 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -338,7 +338,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: CONTINUE_ID, weight: KeybindingWeight.WorkbenchContrib + 10, // Use a stronger weight to get priority over start debugging F5 shortcut primary: KeyCode.F5, - when: CONTEXT_IN_DEBUG_MODE, + when: CONTEXT_DEBUG_STATE.isEqualTo('stopped'), handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { getThreadAndRun(accessor, context, thread => thread.continue()); } @@ -389,7 +389,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DEBUG_START_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.F5, - when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing))), + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.isEqualTo('inactive')), handler: async (accessor: ServicesAccessor, debugStartOptions?: { config?: Partial; noDebug?: boolean }) => { const debugService = accessor.get(IDebugService); let { launch, name, getConfig } = debugService.getConfigurationManager().selectedConfiguration; From 1f912dfa3d5377aea4b90338124f9703b2781556 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 19 May 2021 10:46:33 -0400 Subject: [PATCH 116/270] Fix #123617 --- .../services/editor/browser/editorOverrideService.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 5ca97d5b24c..990a4acf6d9 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -161,8 +161,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride return toDisposable(() => remove()); } - hasContributionPoint(schemeOrGlob: string): boolean { - return this._contributionPoints.has(schemeOrGlob); + hasContributionPoint(glob: string): boolean { + return this._contributionPoints.has(glob); } getAssociationsForResource(resource: URI): EditorAssociations { @@ -223,12 +223,15 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } private findMatchingContributions(resource: URI): ContributionPoint[] { + // The user setting should be respected even if the editor doesn't specify that resource in package.json + const userSettings = this.getAssociationsForResource(resource); let contributions: ContributionPoint[] = []; // Then all glob patterns for (const key of this._contributionPoints.keys()) { const contributionPoints = this._contributionPoints.get(key)!; for (const contributionPoint of contributionPoints) { - if (globMatchesResource(key, resource)) { + const foundInSettings = userSettings.find(setting => setting.viewType === contributionPoint.editorInfo.id); + if (foundInSettings || globMatchesResource(key, resource)) { contributions.push(contributionPoint); } } From eaf5a49200b19759769e09c330a667900825fddd Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 19 May 2021 17:00:06 +0200 Subject: [PATCH 117/270] status - update description of `name` property --- src/vs/vscode.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index e2d046b0db8..53a4a1c5361 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5604,7 +5604,9 @@ declare module 'vscode' { readonly priority?: number; /** - * The (short) name of the entry, like 'Language Indicator', 'Git Status' etc. + * The name of the entry, like 'Python Language Indicator', 'Git Status' etc. + * Try to keep the length of the name short, yet descriptive enough that + * users can understand what the status bar item is about. */ name: string | undefined; From b63ad124d2e59c14ab75a0fc4bdcf28d24736173 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 19 May 2021 17:16:48 +0200 Subject: [PATCH 118/270] mark untitled notebooks as dirty by default, don't hold on to untitled notebooks eagerly, https://github.com/microsoft/vscode/issues/121974 --- .../src/singlefolder-tests/notebook.document.test.ts | 4 ---- .../api/browser/mainThreadNotebookDocuments.ts | 12 +++++++++++- .../notebook/browser/notebook.contribution.ts | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index 0739e4e8afc..568295ccddf 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -47,10 +47,6 @@ suite('Notebook Document', function () { await utils.closeAllEditors(); utils.disposeAll(disposables); disposables.length = 0; - - for (let doc of vscode.notebook.notebookDocuments) { - assert.strictEqual(doc.isDirty, false, doc.uri.toString()); - } }); suiteSetup(function () { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index bad31cf5a11..f557f5e253d 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -132,7 +132,17 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS } const ref = await this._notebookEditorModelResolverService.resolve(uri, options.viewType); - this._modelReferenceCollection.add(ref.object.resource, ref); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + + // untitled notebooks are dirty by default + this._proxy.$acceptDirtyStateChanged(uri, true); + + // apply content changes... slightly HACKY -> this triggers a change event if (options.content) { ref.object.notebook.reset( options.content.cells, diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 7142116ab99..f0051ae83d7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -255,7 +255,7 @@ class CellContentProvider implements ITextModelContentProvider { } if (result) { - const once = result.onWillDispose(() => { + const once = Event.any(result.onWillDispose, ref.object.notebook.onWillDispose)(() => { once.dispose(); ref.dispose(); }); From dc8844925abd2abf1998eb5b4620e816afda6a6f Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 19 May 2021 11:39:13 -0400 Subject: [PATCH 119/270] Part of #122996 --- src/vs/workbench/browser/parts/editor/titleControl.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index e2d0ed9a806..028cf936329 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -290,7 +290,7 @@ export abstract class TitleControl extends Themable { return false; } - const editorOptions: ITextEditorOptions = { + let editorOptions: ITextEditorOptions = { viewState: (() => { if (this.group.activeEditor === editor) { const activeControl = this.group.activeEditorPane?.getControl(); @@ -304,6 +304,14 @@ export abstract class TitleControl extends Themable { sticky: this.group.isSticky(editor) }; + // If it's a custom editor or a notebook add the viewtype + if ((editor as object).hasOwnProperty('viewType')) { + interface EditorInputWithViewType extends IEditorInput { + viewType: string; + } + editorOptions = { ...editorOptions, override: (editor as EditorInputWithViewType).viewType }; + } + this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], () => editorOptions, e); return true; From 9a322a0d62ab32281b9936e601c07548799ff0a9 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 19 May 2021 08:40:09 -0700 Subject: [PATCH 120/270] use folder terminology due to windows/linux limit --- .../contrib/workspace/browser/workspaceTrustEditor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 54b4bc06fba..79109097b1b 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -235,7 +235,7 @@ class WorkspaceTrustedUrisTable extends Disposable { async edit(item: ITrustedUriItem) { if (item.uri.scheme === Schemas.file || item.uri.scheme === Schemas.vscodeRemote) { const uri = await this.fileDialogService.showOpenDialog({ - canSelectFiles: true, + canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: item.uri, @@ -480,8 +480,8 @@ class TrustedUriHostColumnRenderer implements ITableRenderer Date: Wed, 19 May 2021 08:41:55 -0700 Subject: [PATCH 121/270] tweak terminology for wt entries --- .../contrib/workspace/browser/workspaceTrustEditor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 79109097b1b..537356d50b7 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -728,10 +728,10 @@ export class WorkspaceTrustEditor extends EditorPane { private createConfigurationElement(parent: HTMLElement): void { this.configurationContainer = append(parent, $('.workspace-trust-settings')); const configurationTitle = append(this.configurationContainer, $('.workspace-trusted-folders-title')); - configurationTitle.innerText = localize('trustedFolders', "Trusted Folders"); + configurationTitle.innerText = localize('trustedFoldersAndWorkspaces', "Trusted Folders & Workspaces"); const configurationDescription = append(this.configurationContainer, $('.workspace-trusted-folders-description')); - configurationDescription.innerText = localize('trustedFoldersDescription', "You trust the following folders and their children."); + configurationDescription.innerText = localize('trustedFoldersDescription', "You trust the following folders, their children, and workspace files."); this.workpaceTrustedUrisTable = this._register(this.instantiationService.createInstance(WorkspaceTrustedUrisTable, this.configurationContainer)); From 9326ded502b6e541001294eb6a8c61a26e2674b0 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 19 May 2021 18:04:16 +0200 Subject: [PATCH 122/270] show extensions limited due to virtual workspace --- .../platform/extensions/common/extensions.ts | 2 + src/vs/platform/remote/common/remoteHosts.ts | 4 + .../extensions/browser/extensionsViewlet.ts | 22 ++-- .../extensions/browser/extensionsViews.ts | 111 ++++++++++-------- .../workspace/browser/workspaceTrustEditor.ts | 2 +- .../browser/extensionEnablementService.ts | 2 +- .../extensionManifestPropertiesService.ts | 6 +- 7 files changed, 87 insertions(+), 62 deletions(-) diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 2b6d00385f9..3fec343e5eb 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -174,6 +174,8 @@ export type ExtensionKind = 'ui' | 'workspace' | 'web'; export type ExtensionUntrustedWorkpaceSupportType = boolean | 'limited'; export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: 'limited', description: string, restrictedConfigurations?: string[] }; +export type ExtensionVirtualWorkpaceSupportType = boolean | 'partial'; + export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { return thing && typeof thing === 'object' diff --git a/src/vs/platform/remote/common/remoteHosts.ts b/src/vs/platform/remote/common/remoteHosts.ts index 6894d782f5d..ca7c21b3f18 100644 --- a/src/vs/platform/remote/common/remoteHosts.ts +++ b/src/vs/platform/remote/common/remoteHosts.ts @@ -42,3 +42,7 @@ export function getVirtualWorkspaceLocation(workspace: IWorkspace): { scheme: st export function getVirtualWorkspaceScheme(workspace: IWorkspace): string | undefined { return getVirtualWorkspaceLocation(workspace)?.scheme; } + +export function isVirtualWorkspace(workspace: IWorkspace): boolean { + return getVirtualWorkspaceLocation(workspace) !== undefined; +} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 3ecddfc19bb..d90fe39e080 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -22,7 +22,7 @@ import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAct import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, UntrustedWorkspaceUnsupportedExtensionsView, UntrustedWorkspacePartiallySupportedExtensionsView, VirtualWorkspaceUnsupportedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; +import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, UntrustedWorkspaceUnsupportedExtensionsView, UntrustedWorkspacePartiallySupportedExtensionsView, VirtualWorkspaceUnsupportedExtensionsView, VirtualWorkspacePartiallySupportedExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; @@ -54,12 +54,13 @@ import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { URI } from 'vs/base/common/uri'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { VirtualWorkspaceContext, WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { isWeb } from 'vs/base/common/platform'; import { installLocalInRemoteIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustContext } from 'vs/workbench/services/workspaces/common/workspaceTrust'; const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); const SearchIntalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); @@ -70,7 +71,6 @@ const HasInstalledExtensionsContext = new RawContextKey('hasInstalledEx const HasInstalledWebExtensionsContext = new RawContextKey('hasInstalledWebExtensions', false); const BuiltInExtensionsContext = new RawContextKey('builtInExtensions', false); const SearchBuiltInExtensionsContext = new RawContextKey('searchBuiltInExtensions', false); -const UnsupportedWorkspaceExtensionsContext = new RawContextKey('unsupportedWorkspaceExtensions', false); const SearchUnsupportedWorkspaceExtensionsContext = new RawContextKey('searchUnsupportedWorkspaceExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); @@ -412,21 +412,28 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio id: 'workbench.views.extensions.untrustedUnsupportedExtensions', name: localize('untrustedUnsupportedExtensions', "Disabled in Restricted Mode"), ctorDescriptor: new SyncDescriptor(UntrustedWorkspaceUnsupportedExtensionsView, [{}]), - when: ContextKeyExpr.has('unsupportedWorkspaceExtensions'), + when: ContextKeyExpr.and(WorkspaceTrustContext.IsTrusted.negate(), SearchUnsupportedWorkspaceExtensionsContext), }); viewDescriptors.push({ id: 'workbench.views.extensions.untrustedPartiallySupportedExtensions', name: localize('untrustedPartiallySupportedExtensions', "Limited in Restricted Mode"), ctorDescriptor: new SyncDescriptor(UntrustedWorkspacePartiallySupportedExtensionsView, [{}]), - when: ContextKeyExpr.has('unsupportedWorkspaceExtensions'), + when: ContextKeyExpr.and(WorkspaceTrustContext.IsTrusted.negate(), SearchUnsupportedWorkspaceExtensionsContext), }); viewDescriptors.push({ id: 'workbench.views.extensions.virtualUnsupportedExtensions', name: localize('virtualUnsupportedExtensions', "Disabled in Virtual Workspaces"), ctorDescriptor: new SyncDescriptor(VirtualWorkspaceUnsupportedExtensionsView, [{}]), - when: ContextKeyExpr.and(ContextKeyExpr.has('unsupportedWorkspaceExtensions'), ContextKeyExpr.has('virtualWorkspace')), + when: ContextKeyExpr.and(VirtualWorkspaceContext, SearchUnsupportedWorkspaceExtensionsContext), + }); + + viewDescriptors.push({ + id: 'workbench.views.extensions.virtualPartiallySupportedExtensions', + name: localize('virtualPartiallySupportedExtensions', "Limited in Virtual Workspaces"), + ctorDescriptor: new SyncDescriptor(VirtualWorkspacePartiallySupportedExtensionsView, [{}]), + when: ContextKeyExpr.and(VirtualWorkspaceContext, SearchUnsupportedWorkspaceExtensionsContext), }); return viewDescriptors; @@ -447,7 +454,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private hasInstalledWebExtensionsContextKey: IContextKey; private builtInExtensionsContextKey: IContextKey; private searchBuiltInExtensionsContextKey: IContextKey; - private workspaceUnsupportedExtensionsContextKey: IContextKey; private searchWorkspaceUnsupportedExtensionsContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; @@ -485,7 +491,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService); this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); - this.workspaceUnsupportedExtensionsContextKey = UnsupportedWorkspaceExtensionsContext.bindTo(contextKeyService); this.searchWorkspaceUnsupportedExtensionsContextKey = SearchUnsupportedWorkspaceExtensionsContext.bindTo(contextKeyService); this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService); @@ -678,7 +683,6 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchEnabledExtensionsContextKey.set(ExtensionsListView.isEnabledExtensionsQuery(value)); this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value)); this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isSearchBuiltInExtensionsQuery(value)); - this.workspaceUnsupportedExtensionsContextKey.set(ExtensionsListView.isWorkspaceUnsupportedExtensionsQuery(value)); this.searchWorkspaceUnsupportedExtensionsContextKey.set(ExtensionsListView.isSearchWorkspaceUnsupportedExtensionsQuery(value)); this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index e2e6efc6ced..87f38eeb22a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -37,7 +37,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IAction, Action, Separator, ActionRunner } from 'vs/base/common/actions'; -import { ExtensionIdentifier, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType, ExtensionVirtualWorkpaceSupportType, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; @@ -49,7 +49,8 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -129,6 +130,7 @@ export class ExtensionsListView extends ViewPane { @IOpenerService openerService: IOpenerService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IStorageService private readonly storageService: IStorageService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { super({ ...(viewletViewOptions as IViewPaneOptions), @@ -551,45 +553,43 @@ export class ExtensionsListView extends ViewPane { } private filterWorkspaceUnsupportedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { - let value = query.value; - const untrustedPartiallySupportedOnly = /@workspaceUnsupported:untrustedPartial/i.test(value); - if (untrustedPartiallySupportedOnly) { - value = value.replace(/@workspaceUnsupported:untrustedPartial/g, ''); - } - const untrustedUnsupportedOnly = /@workspaceUnsupported:untrusted/i.test(value); - if (untrustedUnsupportedOnly) { - value = value.replace(/@workspaceUnsupported:untrusted/g, ''); - } - const virtualUnsupportedOnly = /@workspaceUnsupported:virtual/i.test(value); - if (virtualUnsupportedOnly) { - value = value.replace(/@workspaceUnsupported:virtual/g, ''); + + // shows local extensions which are restricted or disabled in the current workspace because of the extension's capability + + const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace()); + const inRestrictedWorkspace = !this.workspaceTrustManagementService.isWorkpaceTrusted(); + if (!inVirtualWorkspace && !inRestrictedWorkspace) { + return []; } - value = value.replace(/@workspaceUnsupported/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); + let queryString = query.value; // @sortby is already filtered out - const isVirtualWorkspace = getVirtualWorkspaceScheme(this.workspaceService.getWorkspace()) !== undefined; - const virtualUnsupportedExtensions = local.filter(extension => extension.local && !this.extensionManifestPropertiesService.canSupportVirtualWorkspace(extension.local.manifest) && (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)); + const match = queryString.match(/^\s*@workspaceUnsupported(?::(untrusted|virtual)(Partial)?)?(?:\s+([^\s]*))?/i); + if (!match) { + return []; + } + const type = match[1]?.toLowerCase(); + const partial = !!match[2]; + const nameFilter = match[3]?.toLowerCase(); - let trustRequiringExtensions = local.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) !== true && (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)); - if (isVirtualWorkspace) { - trustRequiringExtensions = trustRequiringExtensions.filter(extension => extension.local && this.extensionManifestPropertiesService.canSupportVirtualWorkspace(extension.local.manifest)); + if (nameFilter) { + local = local.filter(extension => extension.name.toLowerCase().indexOf(nameFilter) > -1 || extension.displayName.toLowerCase().indexOf(nameFilter) > -1); } - if (untrustedUnsupportedOnly) { - const untrustedUnsupportedExtensions = trustRequiringExtensions.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === false); - return this.sortExtensions(untrustedUnsupportedExtensions, options); - } + const hasVirtualSupportType = (extension: IExtension, supportType: ExtensionVirtualWorkpaceSupportType) => extension.local && this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.local.manifest) === supportType; + const hasRestrictedSupportType = (extension: IExtension, supportType: ExtensionUntrustedWorkpaceSupportType) => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === supportType; - if (untrustedPartiallySupportedOnly) { - const untrustedPartiallySupportedExtensions = trustRequiringExtensions.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === 'limited'); - return this.sortExtensions(untrustedPartiallySupportedExtensions, options); + if (type === 'virtual') { + // show limited and disabled extensions unless disabled because of a untrusted workspace + local = local.filter(extension => inVirtualWorkspace && hasVirtualSupportType(extension, partial ? 'partial' : false) && !(inRestrictedWorkspace && hasRestrictedSupportType(extension, false))); + } else if (type === 'untrusted') { + // show limited and disabled extensions unless disabled because of a virtual workspace + local = local.filter(extension => inRestrictedWorkspace && hasRestrictedSupportType(extension, partial ? 'limited' : false) && !(inVirtualWorkspace && hasVirtualSupportType(extension, false))); + } else { + // show extensions that are restricted or disabled in the current workspace + local = local.filter(extension => inVirtualWorkspace && !hasVirtualSupportType(extension, true) || inRestrictedWorkspace && !hasRestrictedSupportType(extension, true)); } - - if (virtualUnsupportedOnly) { - return this.sortExtensions(virtualUnsupportedExtensions, options); - } - - return this.sortExtensions(trustRequiringExtensions, options); + return this.sortExtensions(local, options); } @@ -980,9 +980,7 @@ export class ExtensionsListView extends ViewPane { || this.isBuiltInExtensionsQuery(query) || this.isSearchBuiltInExtensionsQuery(query) || this.isBuiltInGroupExtensionsQuery(query) - || this.isSearchWorkspaceUnsupportedExtensionsQuery(query) - || this.isWorkspaceUnsupportedExtensionsQuery(query) - || this.isWorkspaceUnsupportedGroupExtensionsQuery(query); + || this.isSearchWorkspaceUnsupportedExtensionsQuery(query); } static isSearchBuiltInExtensionsQuery(query: string): boolean { @@ -998,15 +996,7 @@ export class ExtensionsListView extends ViewPane { } static isSearchWorkspaceUnsupportedExtensionsQuery(query: string): boolean { - return /@workspaceUnsupported\s.+/i.test(query); - } - - static isWorkspaceUnsupportedExtensionsQuery(query: string): boolean { - return /^\s*@workspaceUnsupported$/i.test(query.trim()); - } - - static isWorkspaceUnsupportedGroupExtensionsQuery(query: string): boolean { - return /^\s*@workspaceUnsupported:.+$/i.test(query.trim()); + return /^\s*@workspaceUnsupported(:(untrusted|virtual)(Partial)?)?(\s|$)/i.test(query); } static isInstalledExtensionsQuery(query: string): boolean { @@ -1104,21 +1094,46 @@ export class BuiltInProgrammingLanguageExtensionsView extends ExtensionsListView } } +function toSpecificWorkspaceUnsupportedQuery(query: string, qualifier: string): string | undefined { + if (!query) { + return '@workspaceUnsupported:' + qualifier; + } + const match = query.match(new RegExp(`@workspaceUnsupported(:${qualifier})?(\\s|$)`, 'i')); + if (match) { + if (!match[1]) { + return query.replace(/@workspaceUnsupported/gi, '@workspaceUnsupported:' + qualifier); + } + return query; + } + return undefined; +} + + export class UntrustedWorkspaceUnsupportedExtensionsView extends ExtensionsListView { override async show(query: string): Promise> { - return (query && query.trim() !== '@workspaceUnsupported') ? this.showEmptyModel() : super.show('@workspaceUnsupported:untrusted'); + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrusted'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); } } export class UntrustedWorkspacePartiallySupportedExtensionsView extends ExtensionsListView { override async show(query: string): Promise> { - return (query && query.trim() !== '@workspaceUnsupported') ? this.showEmptyModel() : super.show('@workspaceUnsupported:untrustedPartial'); + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrustedPartial'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); } } export class VirtualWorkspaceUnsupportedExtensionsView extends ExtensionsListView { override async show(query: string): Promise> { - return (query && query.trim() !== '@workspaceUnsupported') ? this.showEmptyModel() : super.show('@workspaceUnsupported:virtual'); + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtual'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); + } +} + +export class VirtualWorkspacePartiallySupportedExtensionsView extends ExtensionsListView { + override async show(query: string): Promise> { + const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtualPartial'); + return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel(); } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 08da9da034b..f432c42bf68 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -681,7 +681,7 @@ export class WorkspaceTrustEditor extends EditorPane { const set = new Set(); for (const ext of filtered) { const isVirtualWorkspace = getVirtualWorkspaceScheme(this.workspaceService.getWorkspace()) !== undefined; - if (!isVirtualWorkspace || this.extensionManifestPropertiesService.canSupportVirtualWorkspace(ext.local.manifest)) { + if (!isVirtualWorkspace || this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(ext.local.manifest) !== false) { set.add(ext.identifier.id); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 85bc8459740..4b6e684aeb5 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -230,7 +230,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench private _isDisabledByVirtualWorkspace(extension: IExtension): boolean { if (getVirtualWorkspaceScheme(this.contextService.getWorkspace()) !== undefined) { - return !this.extensionManifestPropertiesService.canSupportVirtualWorkspace(extension.manifest); + return this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.manifest) === false; } return false; } diff --git a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts index 913b0082624..f7966aceeeb 100644 --- a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts +++ b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IExtensionManifest, ExtensionKind, ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionKind, ExtensionIdentifier, ExtensionUntrustedWorkpaceSupportType, ExtensionVirtualWorkpaceSupportType } from 'vs/platform/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { isNonEmptyArray } from 'vs/base/common/arrays'; @@ -30,7 +30,7 @@ export interface IExtensionManifestPropertiesService { getExtensionKind(manifest: IExtensionManifest): ExtensionKind[]; getExtensionUntrustedWorkspaceSupportType(manifest: IExtensionManifest): ExtensionUntrustedWorkpaceSupportType; - canSupportVirtualWorkspace(manifest: IExtensionManifest): boolean; + getExtensionVirtualWorkspaceSupportType(manifest: IExtensionManifest): ExtensionVirtualWorkpaceSupportType; } export class ExtensionManifestPropertiesService extends Disposable implements IExtensionManifestPropertiesService { @@ -156,7 +156,7 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE return false; } - canSupportVirtualWorkspace(manifest: IExtensionManifest): boolean { + getExtensionVirtualWorkspaceSupportType(manifest: IExtensionManifest): ExtensionVirtualWorkpaceSupportType { // check user configured const userConfiguredVirtualWorkspaceSupport = this.getConfiguredVirtualWorkspaceSupport(manifest); if (userConfiguredVirtualWorkspaceSupport !== undefined) { From 81c6572671c63f281e50e02f0da2e38ea943a7ce Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Wed, 19 May 2021 10:15:10 -0600 Subject: [PATCH 123/270] added default editor override for symbol reveal --- src/vs/workbench/contrib/outline/browser/outlinePane.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 8b16ac9dc58..4c0ad0d57f7 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -37,6 +37,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { ITreeSorter } from 'vs/base/browser/ui/tree/tree'; import { URI } from 'vs/base/common/uri'; +import { EditorOverride } from 'vs/platform/editor/common/editor'; const _ctxFollowsCursor = new RawContextKey('outlineFollowsCursor', false); const _ctxFilterOnType = new RawContextKey('outlineFiltersOnType', false); @@ -284,7 +285,7 @@ export class OutlinePane extends ViewPane { // feature: reveal outline selection in editor // on change -> reveal/select defining range - this._editorDisposables.add(tree.onDidOpen(e => newOutline.reveal(e.element, e.editorOptions, e.sideBySide))); + this._editorDisposables.add(tree.onDidOpen(e => newOutline.reveal(e.element, { ...e.editorOptions, ...{ override: EditorOverride.DISABLED } }, e.sideBySide))); // feature: reveal editor selection in outline const revealActiveElement = () => { From 52c8fced3850349a7c1a8f4713c9c021092cd013 Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 19 May 2021 09:16:33 -0700 Subject: [PATCH 124/270] focus indicator border or gutter --- .../notebook/browser/media/notebook.css | 40 -------- .../notebook/browser/notebookEditorWidget.ts | 96 ++++++++++++++++--- .../contrib/notebook/common/notebookCommon.ts | 1 + .../notebook/common/notebookOptions.ts | 18 +++- 4 files changed, 98 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 33bfd31588f..8bf527a75ab 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -347,46 +347,6 @@ display: none; } -/* top and bottom borders on cells */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, -.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, -.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { - content: ""; - position: absolute; - width: 100%; - height: 1px; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - content: ""; - position: absolute; - width: 1px; - height: 100%; - z-index: 10; -} - -/* top border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { - border-top: 1px solid transparent; -} - -/* left border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { - border-left: 1px solid transparent; -} - -/* bottom border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { - border-bottom: 1px solid transparent; -} - -/* right border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - border-right: 1px solid transparent; -} - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { top: 0; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 2307875a22b..d2e55d45fba 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -367,7 +367,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._updateForNotebookConfiguration(); } - if (e.compactView) { + if (e.compactView || e.focusIndicator) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); @@ -555,7 +555,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor bottomCellToolbarGap, bottomCellToolbarHeight, collapsedIndicatorHeight, - compactView + compactView, + focusIndicator } = this._notebookOptions.getLayoutConfiguration(); const styleSheets: string[] = []; @@ -566,6 +567,84 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin}px; }`); } + if (focusIndicator === 'border') { + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { + content: ""; + position: absolute; + width: 100%; + height: 1px; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + content: ""; + position: absolute; + width: 1px; + height: 100%; + z-index: 10; + } + + /* top border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { + border-top: 1px solid transparent; + } + + /* left border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { + border-left: 1px solid transparent; + } + + /* bottom border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { + border-bottom: 1px solid transparent; + } + + /* right border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + border-right: 1px solid transparent; + } + `); + + // left and right border margins + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { + top: -${cellTopMargin}px; height: calc(100% + ${cellTopMargin + cellBottomMargin}px) + }`); + } else { + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + content: ""; + position: absolute; + width: 0px; + height: 100%; + z-index: 10; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { + border-left: 1px solid transparent; + border-right: 1px solid transparent; + border-radius: 2px; + } + `); + + // left and right border margins + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { + top: 0px; height: 100%px; + }`); + } + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${cellRightMargin}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${cellTopMargin}px; }`); @@ -607,15 +686,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor display: none; }`); - // left and right border margins - styleSheets.push(` - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, - .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { - top: -${cellTopMargin}px; height: calc(100% + ${cellTopMargin + cellBottomMargin}px) - }`); - this._styleElement.textContent = styleSheets.join('\n'); } @@ -2609,8 +2679,8 @@ registerThemingParticipant((theme, collector) => { const focusedCellBackgroundColor = theme.getColor(focusedCellBackground); if (focusedCellBackgroundColor) { - collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator, - .notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-collapsed-part { background-color: ${focusedCellBackgroundColor} !important; }`); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 354e926d95b..fc55f06bfef 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -897,6 +897,7 @@ export const ShowCellStatusBarKey = 'notebook.showCellStatusBar'; export const NotebookTextDiffEditorPreview = 'notebook.diff.enablePreview'; export const ExperimentalUseMarkdownRenderer = 'notebook.experimental.useMarkdownRenderer'; export const ExperimentalCompactView = 'notebook.experimental.compactView'; +export const ExperimentalFocusIndicator = 'notebook.experimental.cellFocusIndicator'; export const ExperimentalUndoRedoPerCell = 'notebook.experimental.undoRedoPerCell'; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index bd4cd916a29..7e5f68b509a 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -6,7 +6,7 @@ import { Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalFocusIndicator, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; let EDITOR_TOP_PADDING = 12; const editorTopPaddingChangeEmitter = new Emitter(); @@ -45,6 +45,7 @@ export interface NotebookLayoutConfiguration { cellToolbarLocation: string | { [key: string]: string }; cellToolbarInteraction: string; compactView: boolean; + focusIndicator: 'border' | 'gutter'; } interface NotebookOptionsChangeEvent { @@ -53,6 +54,7 @@ interface NotebookOptionsChangeEvent { cellToolbarInteraction?: boolean; editorTopPadding?: boolean; compactView?: boolean; + focusIndicator?: boolean; } const defaultConfigConstants = { @@ -84,6 +86,7 @@ export class NotebookOptions { const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); const cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); const compactView = this.configurationService.getValue(ExperimentalCompactView); + const focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator) ?? 'border'; this._disposables = []; this._layoutConfiguration = { @@ -103,7 +106,8 @@ export class NotebookOptions { showCellStatusBar, cellToolbarLocation, cellToolbarInteraction, - compactView + compactView, + focusIndicator }; this._disposables.push(this.configurationService.onDidChangeConfiguration(e => { @@ -111,8 +115,9 @@ export class NotebookOptions { let cellToolbarLocation = e.affectsConfiguration(CellToolbarLocKey); let cellToolbarInteraction = e.affectsConfiguration(CellToolbarVisibility); let compactView = e.affectsConfiguration(ExperimentalCompactView); + let focusIndicator = e.affectsConfiguration(ExperimentalFocusIndicator); - if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView) { + if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator) { return; } @@ -130,6 +135,10 @@ export class NotebookOptions { configuration.cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); } + if (focusIndicator) { + configuration.focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator); + } + if (compactView) { const compactViewValue = this.configurationService.getValue('notebook.experimental.compactView'); configuration = Object.assign(configuration, { @@ -145,7 +154,8 @@ export class NotebookOptions { cellStatusBarVisibility: cellStatusBarVisibility, cellToolbarLocation: cellToolbarLocation, cellToolbarInteraction: cellToolbarInteraction, - compactView: compactView + compactView: compactView, + focusIndicator: focusIndicator }); })); From d553c21d5e0413f047f2e4e806ed539c7219e88a Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 19 May 2021 09:33:48 -0700 Subject: [PATCH 125/270] fixes #123869 --- .../contrib/preferences/browser/preferencesRenderers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 06e6fba025e..96eb0132f4a 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -822,7 +822,7 @@ class EditSettingRenderer extends Disposable { private onEditSettingClicked(editPreferenceWidget: EditPreferenceWidget, e: IEditorMouseEvent): void { EventHelper.stop(e.event, true); - const anchor = { x: e.event.posx, y: e.event.posy + 10 }; + const anchor = { x: e.event.posx, y: e.event.posy, height: 10 }; const actions = this.getSettings(editPreferenceWidget.getLine()).length === 1 ? this.getActions(editPreferenceWidget.preferences[0], this.getConfigurationsMap()[editPreferenceWidget.preferences[0].key]) : editPreferenceWidget.preferences.map(setting => new SubmenuAction(`preferences.submenu.${setting.key}`, setting.key, this.getActions(setting, this.getConfigurationsMap()[setting.key]))); this.contextMenuService.showContextMenu({ From 9716c27e068315251a6ed91ac5b60b9d8ee68e86 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 19 May 2021 18:43:56 +0200 Subject: [PATCH 126/270] slightly better uri for untitled notebooks, https://github.com/microsoft/vscode/issues/121974 --- src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostNotebook.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index f557f5e253d..a406d945ee0 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -119,12 +119,12 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS }; } - async $tryCreateNotebook(options: { viewType?: string, content?: NotebookDataDto }): Promise { + async $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise { // find a free URI for the untitled case let uri: URI; for (let counter = 1; ; counter++) { - let candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-Notebook-${counter}` }); + let candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}`, query: options.viewType }); if (!this._notebookService.getNotebookTextModel(candidate)) { uri = candidate; break; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3b12c7d8a37..98368960f85 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -888,7 +888,7 @@ export interface MainThreadNotebookEditorsShape extends IDisposable { } export interface MainThreadNotebookDocumentsShape extends IDisposable { - $tryCreateNotebook(options: { viewType?: string, content?: NotebookDataDto }): Promise; + $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise; $tryOpenNotebook(uriComponents: UriComponents): Promise; $trySaveNotebook(uri: UriComponents): Promise; $applyEdits(resource: UriComponents, edits: IImmediateCellEditOperation[], computeUndoRedo?: boolean): Promise; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index d505510df63..1d2e22cd8ec 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -254,7 +254,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return new NotebookEditorDecorationType(this._notebookEditorsProxy, options).value; } - async createNotebookDocument(options: { viewType?: string, content?: vscode.NotebookData } = {}): Promise { + async createNotebookDocument(options: { viewType: string, content?: vscode.NotebookData }): Promise { const canonicalUri = await this._notebookDocumentsProxy.$tryCreateNotebook({ viewType: options.viewType, content: options.content && typeConverters.NotebookData.from(options.content) From 283180b64b2af567a880280b4e47dca6d8cc1dae Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 19 May 2021 09:54:26 -0700 Subject: [PATCH 127/270] don't show action icons all the time wte --- .../workspace/browser/workspaceTrustEditor.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css index 63f05bb1fd4..63434c78d57 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css @@ -268,3 +268,14 @@ padding-left: 8px; padding-right: 8px; } + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { + display: none; + flex: 1; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-list-row.selected .monaco-table-tr .monaco-table-td .actions .monaco-action-bar, +.workspace-trust-editor .workspace-trust-settings .monaco-table .monaco-list-row.focused .monaco-table-tr .monaco-table-td .actions .monaco-action-bar, +.workspace-trust-editor .workspace-trust-settings .monaco-list-row:hover .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { + display: flex; +} From 45e5c50dbfda3e72d503f8ed1944179b367e2e08 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 10:02:58 -0700 Subject: [PATCH 128/270] Partially implement terminal drag and drop Part of #121500 --- .../terminal/browser/media/terminal.css | 9 +++ .../contrib/terminal/browser/terminal.ts | 15 +++++ .../terminal/browser/terminalInstance.ts | 30 ++++++++- .../terminal/browser/terminalService.ts | 26 ++++++++ .../terminal/browser/terminalTabsList.ts | 64 +++++++++++++++---- 5 files changed, 131 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 12ff01c047e..618fe4256bf 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -289,3 +289,12 @@ margin-left: 4px; color: inherit; } + +.monaco-workbench .pane-body.integrated-terminal .drop-target::after { + background: rgba(128, 128, 255, .5); + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 3edf41f34a6..0e8e9204a31 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -150,6 +150,11 @@ export interface ITerminalService { splitInstance(instance: ITerminalInstance, profile: ITerminalProfile): ITerminalInstance | null; unsplitInstance(instance: ITerminalInstance): void; joinInstances(instances: ITerminalInstance[]): void; + /** + * Moves a terminal instance's group to the target instance group's position. + */ + moveGroup(instance: ITerminalInstance, target: ITerminalInstance): void; + moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'left' | 'right'): void; /** * Perform an action with the active terminal instance, if the terminal does @@ -317,6 +322,11 @@ export interface ITerminalInstance { onFocus: Event; + /** + * An event that fires when a terminal is dropped on this instance via drag and drop. + */ + onRequestAddInstanceToGroup: Event; + /** * Attach a listener to the raw data stream coming from the pty, including ANSI escape * sequences. @@ -595,6 +605,11 @@ export interface ITerminalInstance { changeColor(): Promise; } +export interface IRequestAddInstanceToGroupEvent { + uri: URI; + side: 'left' | 'right' +} + export const enum LinuxDistro { Unknown = 1, Fedora = 2, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 2ea850cfacc..5e93ba564b8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -29,7 +29,7 @@ import { ansiColorIdentifiers, ansiColorMap, TERMINAL_BACKGROUND_COLOR, TERMINAL import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { ITerminalInstanceService, ITerminalInstance, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstanceService, ITerminalInstance, ITerminalExternalLinkProvider, IRequestAddInstanceToGroupEvent } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon, RendererType, ITheme } from 'xterm'; import type { SearchAddon, ISearchOptions } from 'xterm-addon-search'; @@ -216,6 +216,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get onMaximumDimensionsChanged(): Event { return this._onMaximumDimensionsChanged.event; } private readonly _onFocus = new Emitter(); get onFocus(): Event { return this._onFocus.event; } + private readonly _onRequestAddInstanceToGroup = new Emitter(); + get onRequestAddInstanceToGroup(): Event { return this._onRequestAddInstanceToGroup.event; } constructor( private readonly _terminalFocusContextKey: IContextKey, @@ -749,7 +751,20 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._refreshSelectionContextKey(); })); + this._register(dom.addDisposableListener(xterm.element, dom.EventType.DRAG_OVER, (dragEvent: DragEvent) => { + dragEvent.preventDefault(); + if (!dragEvent.dataTransfer) { + return; + } + if ((dragEvent.dataTransfer?.types || []).includes('terminals')) { + this._container?.parentElement?.classList.add('drop-target'); + } + })); + this._register(dom.addDisposableListener(xterm.element, dom.EventType.DRAG_LEAVE, (dragEvent: DragEvent) => { + this._container?.parentElement?.classList.remove('drop-target'); + })); this._register(dom.addDisposableListener(xterm.element, dom.EventType.DROP, async (dragEvent: DragEvent) => { + this._container?.parentElement?.classList.remove('drop-target'); if (!dragEvent.dataTransfer) { return; } @@ -758,7 +773,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { let path: string | undefined; const resources = dragEvent.dataTransfer.getData(DataTransfers.RESOURCES); if (resources) { - path = URI.parse(JSON.parse(resources)[0]).fsPath; + const uri = URI.parse(JSON.parse(resources)[0]); + if (uri.scheme === Schemas.vscodeTerminal) { + console.log('drop event', dragEvent); + this._onRequestAddInstanceToGroup.fire({ + uri, + // TODO: Get side + side: 'right' + }); + return; + } else { + path = uri.fsPath; + } } else if (dragEvent.dataTransfer.files?.[0].path /* Electron only */) { // Check if the file was dragged from the filesystem path = URI.file(dragEvent.dataTransfer.files[0].path).fsPath; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 3fb23678d6a..a4aa765b3c8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -687,6 +687,24 @@ export class TerminalService implements ITerminalService { } } + moveGroup(instance: ITerminalInstance, target: ITerminalInstance): void { + const sourceGroup = this.getGroupForInstance(instance); + const targetGroup = this.getGroupForInstance(target); + if (!sourceGroup || !targetGroup) { + return; + } + const sourceGroupIndex = this._terminalGroups.indexOf(sourceGroup); + const targetGroupIndex = this._terminalGroups.indexOf(targetGroup); + this._terminalGroups.splice(sourceGroupIndex, 1); + this._terminalGroups.splice(targetGroupIndex, 0, sourceGroup); + this._onInstancesChanged.fire(); + } + + moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'left' | 'right'): void { + // TODO: Implement properly + this.joinInstances([source, target]); + } + protected _initInstanceListeners(instance: ITerminalInstance): void { instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed)); instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); @@ -703,6 +721,14 @@ export class TerminalService implements ITerminalService { })); instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onInstanceMaximumDimensionsChanged.fire(instance))); instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged)); + instance.addDisposable(instance.onRequestAddInstanceToGroup(e => { + const sourceInstance = this.getInstanceFromId(parseInt(e.uri.path)); + console.log('source', sourceInstance, 'join', instance); + if (sourceInstance) { + // TODO: Pass in side + this.moveInstance(sourceInstance, instance, e.side); + } + })); } registerProcessSupport(isSupported: boolean): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index aeaa8b5fafb..85c332eac6c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -27,11 +27,12 @@ import { IDecorationsService } from 'vs/workbench/services/decorations/browser/d import { IHoverAction, IHoverService } from 'vs/workbench/services/hover/browser/hover'; import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IListDragAndDrop, IListRenderer } from 'vs/base/browser/ui/list/list'; +import { IListDragAndDrop, IListDragOverReaction, IListRenderer, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; import { disposableTimeout } from 'vs/base/common/async'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; const $ = DOM.$; @@ -78,7 +79,7 @@ export class TerminalTabList extends WorkbenchList { smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), multipleSelectionSupport: true, additionalScrollHeight: TerminalTabsListSizes.TabHeight, - dnd: new TerminalTabsDragAndDrop(_terminalService, _terminalInstanceService) + dnd: instantiationService.createInstance(TerminalTabsDragAndDrop) }, contextKeyService, listService, @@ -403,15 +404,33 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { private _autoFocusDisposable: IDisposable = Disposable.None; constructor( - private _terminalService: ITerminalService, - private _terminalInstanceService: ITerminalInstanceService + @ITerminalService private _terminalService: ITerminalService, + @ITerminalInstanceService private _terminalInstanceService: ITerminalInstanceService ) { } getDragURI(instance: ITerminalInstance): string | null { - return null; + return URI.from({ + scheme: Schemas.vscodeTerminal, + path: instance.instanceId.toString() + }).toString(); } - onDragOver(data: IDragAndDropData, targetInstance: ITerminalInstance | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean { + onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { + if (!originalEvent.dataTransfer) { + return; + } + const dndData: unknown = data.getData(); + if (!Array.isArray(dndData)) { + return; + } + // Attach terminals type to event + const terminals: ITerminalInstance[] = dndData.filter(e => 'instanceId' in (e as any)); + if (terminals.length > 0) { + originalEvent.dataTransfer.setData('terminals', JSON.stringify(terminals.map(e => e.instanceId))); + } + } + + onDragOver(data: IDragAndDropData, targetInstance: ITerminalInstance | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction { let result = true; const didChangeAutoFocusInstance = this._autoFocusInstance !== targetInstance; @@ -424,24 +443,47 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { return result; } - const isExternalDragOver = !(data instanceof ElementsDragAndDropData); - if (didChangeAutoFocusInstance && isExternalDragOver) { + if (didChangeAutoFocusInstance) { this._autoFocusDisposable = disposableTimeout(() => { this._terminalService.setActiveInstance(targetInstance); this._autoFocusInstance = undefined; }, 500); } - return result; + return { + feedback: targetIndex ? [targetIndex] : undefined, + accept: true, + effect: ListDragOverEffect.Move + }; } drop(data: IDragAndDropData, targetInstance: ITerminalInstance | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void { this._autoFocusDisposable.dispose(); this._autoFocusInstance = undefined; - const isExternalDrop = !(data instanceof ElementsDragAndDropData); - if (isExternalDrop) { + if (!(data instanceof ElementsDragAndDropData)) { this._handleExternalDrop(targetInstance, originalEvent); + return; + } + + const draggedElement = data.getData(); + if (!draggedElement || !Array.isArray(draggedElement)) { + return; + } + let focused = false; + if (!targetInstance) { + // TODO: Support dropping on empty + return; + } + for (const e of draggedElement) { + if ('instanceId' in e) { + const instance = e as ITerminalInstance; + this._terminalService.moveGroup(instance, targetInstance); + if (!focused) { + this._terminalService.setActiveInstance(instance); + focused = true; + } + } } } From 49187c4e7854e427dac8fa0128622b935530cb83 Mon Sep 17 00:00:00 2001 From: Raymond Zhao Date: Wed, 19 May 2021 10:03:50 -0700 Subject: [PATCH 129/270] Replace flush with setFlushOn with loglevel info, fixes #123856 --- remote/yarn.lock | 6 +++--- src/vs/platform/log/node/spdlogLog.ts | 3 ++- src/vs/workbench/api/node/extHostOutputService.ts | 1 - yarn.lock | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/remote/yarn.lock b/remote/yarn.lock index 69069f99d84..efeb6c53ebc 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -437,9 +437,9 @@ socks@^2.3.3: smart-buffer "^4.1.0" spdlog@^0.13.0: - version "0.13.4" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.4.tgz#7393d436f077fca1d07500741e50cbf8928a838a" - integrity sha512-tdzk9ysc640emskx+pE/A2JdJ5IAr440ZIsNjRlD9aPK6U6IQ94VUGpl7u0NHamAB8O1H7RxLgtHyXT32V+RaA== + version "0.13.5" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.5.tgz#a31027dcccbe032e9a53579f42cb45428af08bad" + integrity sha512-D1xA5tRXw7eZOoFBCAnOxCxLN3JpHVDjpPJG/xjJ0nFZvtfOUTAzK66MVxJCDht/ZFwjLcBAltvzjfz4JTuSEw== dependencies: bindings "^1.5.0" mkdirp "^0.5.5" diff --git a/src/vs/platform/log/node/spdlogLog.ts b/src/vs/platform/log/node/spdlogLog.ts index e9fd6a709ee..61c411cc675 100644 --- a/src/vs/platform/log/node/spdlogLog.ts +++ b/src/vs/platform/log/node/spdlogLog.ts @@ -11,6 +11,7 @@ async function createSpdLogLogger(name: string, logfilePath: string, filesize: n // Do not crash if spdlog cannot be loaded try { const _spdlog = await import('spdlog'); + _spdlog.setFlushOn(LogLevel.Info); return _spdlog.createAsyncRotatingLogger(name, logfilePath, filesize, filecount); } catch (e) { console.error(e); @@ -20,6 +21,7 @@ async function createSpdLogLogger(name: string, logfilePath: string, filesize: n export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): Promise { const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog'); + _spdlog.setFlushOn(LogLevel.Info); return _spdlog.createRotatingLogger(name, filename, filesize, filecount); } @@ -38,7 +40,6 @@ function log(logger: spdlog.Logger, level: LogLevel, message: string): void { case LogLevel.Critical: logger.critical(message); break; default: throw new Error('Invalid log level'); } - logger.flush(); } export class SpdLogLogger extends AbstractMessageLogger implements ILogger { diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 8daae98da4e..78a955a7330 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -32,7 +32,6 @@ class OutputAppender { append(content: string): void { this.appender.critical(content); - this.flush(); } flush(): void { diff --git a/yarn.lock b/yarn.lock index 02c65997a24..8359b7fa542 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8692,9 +8692,9 @@ sparkles@^1.0.0: integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== spdlog@^0.13.0: - version "0.13.4" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.4.tgz#7393d436f077fca1d07500741e50cbf8928a838a" - integrity sha512-tdzk9ysc640emskx+pE/A2JdJ5IAr440ZIsNjRlD9aPK6U6IQ94VUGpl7u0NHamAB8O1H7RxLgtHyXT32V+RaA== + version "0.13.5" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.5.tgz#a31027dcccbe032e9a53579f42cb45428af08bad" + integrity sha512-D1xA5tRXw7eZOoFBCAnOxCxLN3JpHVDjpPJG/xjJ0nFZvtfOUTAzK66MVxJCDht/ZFwjLcBAltvzjfz4JTuSEw== dependencies: bindings "^1.5.0" mkdirp "^0.5.5" From 6356613d8092a18ef4b9b9dbf9d4c8a9a218f433 Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Wed, 19 May 2021 11:27:45 -0600 Subject: [PATCH 130/270] Update according to suggestion Co-authored-by: Matt Bierner --- src/vs/workbench/contrib/outline/browser/outlinePane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 4c0ad0d57f7..096e0a0d8e3 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -285,7 +285,7 @@ export class OutlinePane extends ViewPane { // feature: reveal outline selection in editor // on change -> reveal/select defining range - this._editorDisposables.add(tree.onDidOpen(e => newOutline.reveal(e.element, { ...e.editorOptions, ...{ override: EditorOverride.DISABLED } }, e.sideBySide))); + this._editorDisposables.add(tree.onDidOpen(e => newOutline.reveal(e.element, { ...e.editorOptions, override: EditorOverride.DISABLED }, e.sideBySide))); // feature: reveal editor selection in outline const revealActiveElement = () => { From 5442f154ef67d6930f778b6604f170339f493612 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 19 May 2021 10:31:07 -0700 Subject: [PATCH 131/270] fix issue when not connected to remote #wt --- .../contrib/workspace/browser/workspaceTrustEditor.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 537356d50b7..db2d0416960 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -175,9 +175,12 @@ class WorkspaceTrustedUrisTable extends Disposable { } } + private get currentWorkspaceUri(): URI { + return this.workspaceService.getWorkspace().folders[0]?.uri || URI.file('/'); + } + private get trustedUriEntries(): ITrustedUriItem[] { const currentWorkspace = this.workspaceService.getWorkspace(); - const currentWorkspaceUri = currentWorkspace.folders[0]?.uri || URI.file('/'); const currentWorkspaceUris = currentWorkspace.folders.map(folder => folder.uri); if (currentWorkspace.configuration) { currentWorkspaceUris.push(currentWorkspace.configuration); @@ -196,7 +199,7 @@ class WorkspaceTrustedUrisTable extends Disposable { parentOfWorkspaceItem: relatedToCurrentWorkspace }; }); - entries.push({ uri: currentWorkspaceUri, entryType: TrustedUriItemType.Add, parentOfWorkspaceItem: false }); + entries.push({ uri: this.currentWorkspaceUri, entryType: TrustedUriItemType.Add, parentOfWorkspaceItem: false }); return entries; } @@ -233,7 +236,9 @@ class WorkspaceTrustedUrisTable extends Disposable { } async edit(item: ITrustedUriItem) { - if (item.uri.scheme === Schemas.file || item.uri.scheme === Schemas.vscodeRemote) { + const canUseOpenDialog = item.uri.scheme === Schemas.file || + (item.uri.scheme === this.currentWorkspaceUri.scheme && this.uriService.extUri.isEqualAuthority(this.currentWorkspaceUri.authority, item.uri.authority)); + if (canUseOpenDialog) { const uri = await this.fileDialogService.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, From 3804c98d6f7bf2250a39d55228c5d0fd0398bbfd Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 19 May 2021 10:35:58 -0700 Subject: [PATCH 132/270] fix padding for table #wt --- .../contrib/workspace/browser/workspaceTrustEditor.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css index 63434c78d57..b26d4cc9aa5 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css @@ -217,8 +217,7 @@ } .workspace-trust-editor .workspace-trust-settings { - padding-top: 20px; - padding-bottom: 20px; + padding: 20px 14px; } .workspace-trust-editor .workspace-trust-settings .workspace-trusted-folders-title { From f36c7a0860b60b5f432101f65cf8308838575b45 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 10:48:14 -0700 Subject: [PATCH 133/270] Terminal dnd feedback --- .../terminal/browser/media/terminal.css | 15 +- .../terminal/browser/terminalInstance.ts | 144 ++++++++++++------ 2 files changed, 110 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 618fe4256bf..978d0b63529 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -290,11 +290,20 @@ color: inherit; } -.monaco-workbench .pane-body.integrated-terminal .drop-target::after { +.monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay { background: rgba(128, 128, 255, .5); - content: ''; display: block; position: absolute; - width: 100%; + left: 0; + right: 0; height: 100%; + pointer-events: none; + opacity: 0; /* hidden initially */ + transition: left 70ms ease-out, right 70ms ease-out, opacity 150ms ease-out; +} +.monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay.drop-left { + right: 50%; +} +.monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay.drop-right { + left: 50% } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 5e93ba564b8..3360c6681c1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -58,6 +58,7 @@ import { isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/plat import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { DataTransfers } from 'vs/base/browser/dnd'; +import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -105,6 +106,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _shellType: TerminalShellType; private _title: string = ''; private _container: HTMLElement | undefined; + private _dropOverlay: HTMLElement | undefined; private _wrapperElement: (HTMLElement & { xterm?: XTermTerminal }) | undefined; private _xterm: XTermTerminal | undefined; private _xtermCore: XTermCore | undefined; @@ -751,54 +753,95 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._refreshSelectionContextKey(); })); - this._register(dom.addDisposableListener(xterm.element, dom.EventType.DRAG_OVER, (dragEvent: DragEvent) => { - dragEvent.preventDefault(); - if (!dragEvent.dataTransfer) { - return; - } - if ((dragEvent.dataTransfer?.types || []).includes('terminals')) { - this._container?.parentElement?.classList.add('drop-target'); - } - })); - this._register(dom.addDisposableListener(xterm.element, dom.EventType.DRAG_LEAVE, (dragEvent: DragEvent) => { - this._container?.parentElement?.classList.remove('drop-target'); - })); - this._register(dom.addDisposableListener(xterm.element, dom.EventType.DROP, async (dragEvent: DragEvent) => { - this._container?.parentElement?.classList.remove('drop-target'); - if (!dragEvent.dataTransfer) { - return; - } - - // Check if files were dragged from the tree explorer - let path: string | undefined; - const resources = dragEvent.dataTransfer.getData(DataTransfers.RESOURCES); - if (resources) { - const uri = URI.parse(JSON.parse(resources)[0]); - if (uri.scheme === Schemas.vscodeTerminal) { - console.log('drop event', dragEvent); - this._onRequestAddInstanceToGroup.fire({ - uri, - // TODO: Get side - side: 'right' - }); - return; - } else { - path = uri.fsPath; + this._register(this._register(new DragAndDropObserver(this._container.parentElement!, { + onDragEnter: e => { + if (!this._dropOverlay) { + this._dropOverlay = document.createElement('div'); + this._dropOverlay.classList.add('terminal-drop-overlay'); } - } else if (dragEvent.dataTransfer.files?.[0].path /* Electron only */) { - // Check if the file was dragged from the filesystem - path = URI.file(dragEvent.dataTransfer.files[0].path).fsPath; + + const types = e.dataTransfer?.types || []; + + // Dragging terminals + if (types.includes('terminals')) { + const side = this._getDropSide(e); + this._dropOverlay.classList.toggle('drop-left', side === 'left'); + this._dropOverlay.classList.toggle('drop-right', side === 'right'); + } + + if (!this._dropOverlay.parentElement) { + this._container?.parentElement?.appendChild(this._dropOverlay); + } + }, + onDragLeave: e => { + if (this._dropOverlay && this._dropOverlay.parentElement) { + this._dropOverlay.parentElement.removeChild(this._dropOverlay); + } + this._dropOverlay = undefined; + }, + onDragEnd: e => { + if (this._dropOverlay && this._dropOverlay.parentElement) { + this._dropOverlay.parentElement.removeChild(this._dropOverlay); + } + this._dropOverlay = undefined; + }, + onDragOver: e => { + if (!e.dataTransfer || !this._dropOverlay) { + return; + } + + const types = e.dataTransfer?.types || []; + + // Dragging terminals + if (types.includes('terminals')) { + console.log('side', this._getDropSide(e)); + const side = this._getDropSide(e); + this._dropOverlay.classList.toggle('drop-left', side === 'left'); + this._dropOverlay.classList.toggle('drop-right', side === 'right'); + } + + this._dropOverlay.style.opacity = '1'; + }, + + onDrop: async e => { + if (this._dropOverlay && this._dropOverlay.parentElement) { + this._dropOverlay.parentElement.removeChild(this._dropOverlay); + } + this._dropOverlay = undefined; + + if (!e.dataTransfer) { + return; + } + + // Check if files were dragged from the tree explorer + let path: string | undefined; + const resources = e.dataTransfer.getData(DataTransfers.RESOURCES); + if (resources) { + const uri = URI.parse(JSON.parse(resources)[0]); + if (uri.scheme === Schemas.vscodeTerminal) { + this._onRequestAddInstanceToGroup.fire({ + uri, + side: this._getDropSide(e) + }); + return; + } else { + path = uri.fsPath; + } + } else if (e.dataTransfer.files?.[0].path /* Electron only */) { + // Check if the file was dragged from the filesystem + path = URI.file(e.dataTransfer.files[0].path).fsPath; + } + + if (!path) { + return; + } + + const preparedPath = await this._terminalInstanceService.preparePathForTerminalAsync(path, this.shellLaunchConfig.executable, this.title, this.shellType, this.isRemote); + + this.sendText(preparedPath, false); + this.focus(); } - - if (!path) { - return; - } - - const preparedPath = await this._terminalInstanceService.preparePathForTerminalAsync(path, this.shellLaunchConfig.executable, this.title, this.shellType, this.isRemote); - - this.sendText(preparedPath, false); - this.focus(); - })); + }))); this._widgetManager.attachToElement(xterm.element); this._processManager.onProcessReady(() => this._linkManager?.setWidgetManager(this._widgetManager)); @@ -820,6 +863,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + private _getDropSide(e: DragEvent): 'left' | 'right' { + const target = this._container?.parentElement; + if (!target) { + return 'right'; + } + const rect = target.getBoundingClientRect(); + return e.clientX - rect.left < rect.width / 2 ? 'left' : 'right'; + } + private async _measureRenderTime(): Promise { await this._xtermReadyPromise; const frameTimes: number[] = []; From a796ebfb8b41db1890e9e55a1b8fdc34dbf99369 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 19 May 2021 10:48:44 -0700 Subject: [PATCH 134/270] update actions column width #wt --- .../contrib/workspace/browser/workspaceTrustEditor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index db2d0416960..82016b8c8d1 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -119,8 +119,8 @@ class WorkspaceTrustedUrisTable extends Disposable { label: '', tooltip: '', weight: 0, - minimumWidth: 80, - maximumWidth: 80, + minimumWidth: 55, + maximumWidth: 55, templateId: TrustedUriActionsColumnRenderer.TEMPLATE_ID, project(row: ITrustedUriItem): ITrustedUriItem { return row; } }, From 134d9b187bbac1ef64be25ca805d210208a2c0aa Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 19 May 2021 13:52:32 -0400 Subject: [PATCH 135/270] Closes #122433 --- .../editor/browser/editorOverrideService.ts | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 990a4acf6d9..45c897382db 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -40,9 +40,13 @@ type ContributionPoints = Array; export class EditorOverrideService extends Disposable implements IEditorOverrideService { readonly _serviceBrand: undefined; + // Constants private static readonly configureDefaultID = 'promptOpenWith.configureDefault'; - private _contributionPoints: Map = new Map(); private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; + private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; + + // Data Stores + private _contributionPoints: Map = new Map(); private cache: Set | undefined; constructor( @@ -129,8 +133,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } const input = await this.doOverrideEditorInput(editor, options, group, selectedContribution); if (conflictingDefault && input) { - // Wait one second to give the user ample time to see the current editor then ask them to configure a default - this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); + // Show the conflicting default dialog + await this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); } // Add the group as we might've changed it with the quickpick @@ -367,12 +371,23 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } private async doHandleConflictingDefaults(editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { - const makeCurrentEditorDefault = () => { - const viewType = currentEditor.viewType; - if (viewType) { - this.updateUserAssociations(`*${extname(currentEditor.resource!)}`, viewType); - } + type StoredChoice = { + [key: string]: string[]; }; + const contributionPoints = this.findMatchingContributions(currentEditor.resource!); + const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorOverrideService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); + const globForResource = `*${extname(currentEditor.resource!)}`; + // Writes to the storage service that a choice has been made for the currently installed editors + const writeCurrentEditorsToStorage = () => { + storedChoices[globForResource] = []; + contributionPoints.forEach(contrib => storedChoices[globForResource].push(contrib.editorInfo.id)); + this.storageService.store(EditorOverrideService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); + }; + + // If the user has already made a choice for this editor we don't want to ask them again + if (storedChoices[globForResource]?.find(editorID => editorID === currentEditor.viewType)) { + return; + } const handle = this.notificationService.prompt(Severity.Warning, localize('editorOverride.conflictingDefaults', 'There are multiple default editors available for the resource.'), @@ -400,12 +415,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }, { label: localize('editorOverride.keepDefault', 'Keep {0}', editorName), - run: makeCurrentEditorDefault + run: writeCurrentEditorsToStorage } ]); // If the user pressed X we assume they want to keep the current editor as default const onCloseListener = handle.onDidClose(() => { - makeCurrentEditorDefault(); + writeCurrentEditorsToStorage(); onCloseListener.dispose(); }); } From f4e05837d01f1bd2405ce0b6842e473a01c0264d Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 May 2021 10:58:25 -0700 Subject: [PATCH 136/270] Remove custom height, use default from menu service #123869 --- .../contrib/preferences/browser/preferencesRenderers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 96eb0132f4a..be7329df4c1 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -822,7 +822,7 @@ class EditSettingRenderer extends Disposable { private onEditSettingClicked(editPreferenceWidget: EditPreferenceWidget, e: IEditorMouseEvent): void { EventHelper.stop(e.event, true); - const anchor = { x: e.event.posx, y: e.event.posy, height: 10 }; + const anchor = { x: e.event.posx, y: e.event.posy }; const actions = this.getSettings(editPreferenceWidget.getLine()).length === 1 ? this.getActions(editPreferenceWidget.preferences[0], this.getConfigurationsMap()[editPreferenceWidget.preferences[0].key]) : editPreferenceWidget.preferences.map(setting => new SubmenuAction(`preferences.submenu.${setting.key}`, setting.key, this.getActions(setting, this.getConfigurationsMap()[setting.key]))); this.contextMenuService.showContextMenu({ From 5266a5fae3f840af69214b2a0ede154a9579816c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 11:17:25 -0700 Subject: [PATCH 137/270] Improved hover feedback --- .../terminal/browser/terminalInstance.ts | 220 ++++++++++-------- 1 file changed, 120 insertions(+), 100 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 3360c6681c1..2708a3e685c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -58,7 +58,7 @@ import { isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/plat import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { DataTransfers } from 'vs/base/browser/dnd'; -import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { DragAndDropObserver, IDragAndDropObserverCallbacks } from 'vs/workbench/browser/dnd'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -106,7 +106,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _shellType: TerminalShellType; private _title: string = ''; private _container: HTMLElement | undefined; - private _dropOverlay: HTMLElement | undefined; private _wrapperElement: (HTMLElement & { xterm?: XTermTerminal }) | undefined; private _xterm: XTermTerminal | undefined; private _xtermCore: XTermCore | undefined; @@ -753,95 +752,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._refreshSelectionContextKey(); })); - this._register(this._register(new DragAndDropObserver(this._container.parentElement!, { - onDragEnter: e => { - if (!this._dropOverlay) { - this._dropOverlay = document.createElement('div'); - this._dropOverlay.classList.add('terminal-drop-overlay'); - } - - const types = e.dataTransfer?.types || []; - - // Dragging terminals - if (types.includes('terminals')) { - const side = this._getDropSide(e); - this._dropOverlay.classList.toggle('drop-left', side === 'left'); - this._dropOverlay.classList.toggle('drop-right', side === 'right'); - } - - if (!this._dropOverlay.parentElement) { - this._container?.parentElement?.appendChild(this._dropOverlay); - } - }, - onDragLeave: e => { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } - this._dropOverlay = undefined; - }, - onDragEnd: e => { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } - this._dropOverlay = undefined; - }, - onDragOver: e => { - if (!e.dataTransfer || !this._dropOverlay) { - return; - } - - const types = e.dataTransfer?.types || []; - - // Dragging terminals - if (types.includes('terminals')) { - console.log('side', this._getDropSide(e)); - const side = this._getDropSide(e); - this._dropOverlay.classList.toggle('drop-left', side === 'left'); - this._dropOverlay.classList.toggle('drop-right', side === 'right'); - } - - this._dropOverlay.style.opacity = '1'; - }, - - onDrop: async e => { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } - this._dropOverlay = undefined; - - if (!e.dataTransfer) { - return; - } - - // Check if files were dragged from the tree explorer - let path: string | undefined; - const resources = e.dataTransfer.getData(DataTransfers.RESOURCES); - if (resources) { - const uri = URI.parse(JSON.parse(resources)[0]); - if (uri.scheme === Schemas.vscodeTerminal) { - this._onRequestAddInstanceToGroup.fire({ - uri, - side: this._getDropSide(e) - }); - return; - } else { - path = uri.fsPath; - } - } else if (e.dataTransfer.files?.[0].path /* Electron only */) { - // Check if the file was dragged from the filesystem - path = URI.file(e.dataTransfer.files[0].path).fsPath; - } - - if (!path) { - return; - } - - const preparedPath = await this._terminalInstanceService.preparePathForTerminalAsync(path, this.shellLaunchConfig.executable, this.title, this.shellType, this.isRemote); - - this.sendText(preparedPath, false); - this.focus(); - } - }))); + const dndController = new TerminalInstanceDropAndDropController(this._container.parentElement!); + dndController.onDropTerminal(e => this._onRequestAddInstanceToGroup.fire(e)); + dndController.onDropFile(async path => { + const preparedPath = await this._terminalInstanceService.preparePathForTerminalAsync(path, this.shellLaunchConfig.executable, this.title, this.shellType, this.isRemote); + this.sendText(preparedPath, false); + this.focus(); + }); + this._register(new DragAndDropObserver(this._container.parentElement!, dndController)); this._widgetManager.attachToElement(xterm.element); this._processManager.onProcessReady(() => this._linkManager?.setWidgetManager(this._widgetManager)); @@ -863,15 +781,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - private _getDropSide(e: DragEvent): 'left' | 'right' { - const target = this._container?.parentElement; - if (!target) { - return 'right'; - } - const rect = target.getBoundingClientRect(); - return e.clientX - rect.left < rect.width / 2 ? 'left' : 'right'; - } - private async _measureRenderTime(): Promise { await this._xtermReadyPromise; const frameTimes: number[] = []; @@ -1915,6 +1824,117 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } +class TerminalInstanceDropAndDropController implements IDragAndDropObserverCallbacks { + private _dropOverlay?: HTMLElement; + + + private readonly _onDropFile = new Emitter(); + get onDropFile(): Event { return this._onDropFile.event; } + private readonly _onDropTerminal = new Emitter(); + get onDropTerminal(): Event { return this._onDropTerminal.event; } + + constructor( + private readonly _container: HTMLElement + ) { + } + + onDragEnter(e: DragEvent) { + if (!this._dropOverlay) { + this._dropOverlay = document.createElement('div'); + this._dropOverlay.classList.add('terminal-drop-overlay'); + } + + const types = e.dataTransfer?.types || []; + + // Dragging terminals + if (types.includes('terminals')) { + const side = this._getDropSide(e); + this._dropOverlay.classList.toggle('drop-left', side === 'left'); + this._dropOverlay.classList.toggle('drop-right', side === 'right'); + } + + if (!this._dropOverlay.parentElement) { + this._container.appendChild(this._dropOverlay); + } + } + onDragLeave(e: DragEvent) { + if (this._dropOverlay && this._dropOverlay.parentElement) { + this._dropOverlay.parentElement.removeChild(this._dropOverlay); + } + this._dropOverlay = undefined; + } + + onDragEnd(e: DragEvent) { + if (this._dropOverlay && this._dropOverlay.parentElement) { + this._dropOverlay.parentElement.removeChild(this._dropOverlay); + } + this._dropOverlay = undefined; + } + + onDragOver(e: DragEvent) { + if (!e.dataTransfer || !this._dropOverlay) { + return; + } + + const types = e.dataTransfer?.types || []; + + // Dragging terminals + if (types.includes('terminals')) { + console.log('side', this._getDropSide(e)); + const side = this._getDropSide(e); + this._dropOverlay.classList.toggle('drop-left', side === 'left'); + this._dropOverlay.classList.toggle('drop-right', side === 'right'); + } + + this._dropOverlay.style.opacity = '1'; + } + + async onDrop(e: DragEvent) { + if (this._dropOverlay && this._dropOverlay.parentElement) { + this._dropOverlay.parentElement.removeChild(this._dropOverlay); + } + this._dropOverlay = undefined; + + if (!e.dataTransfer) { + return; + } + + // Check if files were dragged from the tree explorer + let path: string | undefined; + const resources = e.dataTransfer.getData(DataTransfers.RESOURCES); + if (resources) { + const uri = URI.parse(JSON.parse(resources)[0]); + if (uri.scheme === Schemas.vscodeTerminal) { + this._onDropTerminal.fire({ + uri, + side: this._getDropSide(e) + }); + return; + } else { + path = uri.fsPath; + } + } else if (e.dataTransfer.files?.[0].path /* Electron only */) { + // Check if the file was dragged from the filesystem + path = URI.file(e.dataTransfer.files[0].path).fsPath; + } + + if (!path) { + return; + } + + this._onDropFile.fire(path); + } + + private _getDropSide(e: DragEvent): 'left' | 'right' { + const target = this._container; + if (!target) { + return 'right'; + } + const rect = target.getBoundingClientRect(); + return e.clientX - rect.left < rect.width / 2 ? 'left' : 'right'; + } +} + let colors: string[] = []; registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { // add icon colors From 25a12c75c5afae25e2c8c7b2f6993b6a8aa477a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 19 May 2021 20:40:03 +0200 Subject: [PATCH 138/270] use dummy uri --- extensions/github-authentication/src/githubServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index a3124b7c64b..937f47a681a 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -49,7 +49,7 @@ export class GitHubServer { // TODO@joaomoreno TODO@RMacfarlane private async isNoCorsEnvironment(): Promise { - const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); + const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); return uri.scheme === 'https' && /^vscode\./.test(uri.authority); } From 2b30689ab00bfcc24e2072eb1dbbe3f2a582a359 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 19 May 2021 11:44:20 -0700 Subject: [PATCH 139/270] Close #123935 --- .github/commands.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/commands.json b/.github/commands.json index de0643d56c9..65936b422a9 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -133,6 +133,18 @@ "action": "updateLabels", "addLabel": "~needs more info" }, + { + "type": "comment", + "name": "needsPerfInfo", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "addLabel": "needs more info", + "comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!" + }, { "type": "comment", "name": "jsDebugLogs", From f1a372c15f885552f7d6a0b16635a63d2df54cd7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 12:00:43 -0700 Subject: [PATCH 140/270] Support rearranging splits --- .../contrib/terminal/browser/terminal.ts | 3 ++- .../contrib/terminal/browser/terminalGroup.ts | 10 ++++++++ .../terminal/browser/terminalService.ts | 24 +++++++++++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 0e8e9204a31..632674fa598 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -77,6 +77,7 @@ export interface ITerminalGroup { attachToElement(element: HTMLElement): void; addInstance(instance: ITerminalInstance): void; removeInstance(instance: ITerminalInstance): void; + moveInstance(instance: ITerminalInstance, index: number): void; setVisible(visible: boolean): void; layout(width: number, height: number): void; addDisposable(disposable: IDisposable): void; @@ -153,7 +154,7 @@ export interface ITerminalService { /** * Moves a terminal instance's group to the target instance group's position. */ - moveGroup(instance: ITerminalInstance, target: ITerminalInstance): void; + moveGroup(source: ITerminalInstance, target: ITerminalInstance): void; moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'left' | 'right'): void; /** diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 46b266289fa..27dbf932e67 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -379,6 +379,16 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } } + moveInstance(instance: ITerminalInstance, index: number): void { + const sourceIndex = this.terminalInstances.indexOf(instance); + if (sourceIndex === -1) { + return; + } + this._terminalInstances.splice(sourceIndex, 1); + this._terminalInstances.splice(index, 0, instance); + this._onInstancesChanged.fire(); + } + private _setActiveInstance(instance: ITerminalInstance) { this.setActiveInstanceByIndex(this._getIndexFromId(instance.instanceId)); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index a4aa765b3c8..b01bceef1cb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -687,8 +687,8 @@ export class TerminalService implements ITerminalService { } } - moveGroup(instance: ITerminalInstance, target: ITerminalInstance): void { - const sourceGroup = this.getGroupForInstance(instance); + moveGroup(source: ITerminalInstance, target: ITerminalInstance): void { + const sourceGroup = this.getGroupForInstance(source); const targetGroup = this.getGroupForInstance(target); if (!sourceGroup || !targetGroup) { return; @@ -701,6 +701,24 @@ export class TerminalService implements ITerminalService { } moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'left' | 'right'): void { + const sourceGroup = this.getGroupForInstance(source); + const targetGroup = this.getGroupForInstance(target); + if (!sourceGroup || !targetGroup) { + return; + } + + // Rearrange within the same group + if (sourceGroup === targetGroup) { + // Rearrange + const index = targetGroup.terminalInstances.indexOf(target) + (side === 'right' ? 1 : 0); + targetGroup.moveInstance(source, index); + return; + } + + // Move groups + sourceGroup.removeInstance(source); + + // TODO: Implement properly this.joinInstances([source, target]); } @@ -723,9 +741,7 @@ export class TerminalService implements ITerminalService { instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged)); instance.addDisposable(instance.onRequestAddInstanceToGroup(e => { const sourceInstance = this.getInstanceFromId(parseInt(e.uri.path)); - console.log('source', sourceInstance, 'join', instance); if (sourceInstance) { - // TODO: Pass in side this.moveInstance(sourceInstance, instance, e.side); } })); From e071cefa4dfa90f611ef4215e1203de8ee1d6289 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 12:03:13 -0700 Subject: [PATCH 141/270] Support creating splits in different groups --- .../terminal/browser/terminalService.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index b01bceef1cb..ab0e1e585c6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -707,20 +707,16 @@ export class TerminalService implements ITerminalService { return; } - // Rearrange within the same group - if (sourceGroup === targetGroup) { - // Rearrange - const index = targetGroup.terminalInstances.indexOf(target) + (side === 'right' ? 1 : 0); - targetGroup.moveInstance(source, index); - return; + // Move from the source group to the target group + if (sourceGroup !== targetGroup) { + // Move groups + sourceGroup.removeInstance(source); + targetGroup.addInstance(source); } - // Move groups - sourceGroup.removeInstance(source); - - - // TODO: Implement properly - this.joinInstances([source, target]); + // Rearrange within the target group + const index = targetGroup.terminalInstances.indexOf(target) + (side === 'right' ? 1 : 0); + targetGroup.moveInstance(source, index); } protected _initInstanceListeners(instance: ITerminalInstance): void { From 40fbf5d915be79e03586153f06541ea307d201aa Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 19 May 2021 12:03:52 -0700 Subject: [PATCH 142/270] insertToolbarPosition --- .../contrib/notebook/browser/constants.ts | 8 --- .../notebook/browser/contrib/coreActions.ts | 12 +++- .../notebook/browser/notebookEditorWidget.ts | 66 +++++++++++------- .../notebook/browser/view/notebookCellList.ts | 9 ++- .../browser/view/renderers/cellDnd.ts | 4 +- .../browser/view/renderers/cellRenderer.ts | 4 +- .../browser/viewModel/baseCellViewModel.ts | 2 +- .../browser/viewModel/codeCellViewModel.ts | 12 ++-- .../viewModel/markdownCellViewModel.ts | 6 +- .../contrib/notebook/common/notebookCommon.ts | 1 + .../notebook/common/notebookOptions.ts | 68 +++++++++++++++---- .../notebook/test/notebookCellList.test.ts | 21 +++--- 12 files changed, 140 insertions(+), 73 deletions(-) delete mode 100644 src/vs/workbench/contrib/notebook/browser/constants.ts diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts deleted file mode 100644 index f38ad088c9f..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Scrollable Element - -export const SCROLLABLE_ELEMENT_PADDING_TOP = 18; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index f8e5427ebdc..2a79d2bf867 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -1039,7 +1039,11 @@ MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { }, order: -5, group: 'navigation/add', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarPosition', 'betweenCells'), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarPosition', 'hidden') + ) }); MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { @@ -1105,7 +1109,11 @@ MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { }, order: -5, group: 'navigation/add', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarPosition', 'betweenCells'), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarPosition', 'hidden') + ) }); MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index d2e55d45fba..077b33d94c3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -70,7 +70,6 @@ import { readFontInfo } from 'vs/editor/browser/config/configuration'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; -import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/notebookEditorToolbar'; @@ -213,7 +212,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _dndController: CellDragAndDropController | null = null; private _listTopCellToolbar: ListTopCellToolbar | null = null; private _renderedEditors: Map = new Map(); - private _viewContext: ViewContext | undefined; + private _viewContext: ViewContext; private _notebookViewModel: NotebookViewModel | undefined; private _localStore: DisposableStore = this._register(new DisposableStore()); private _localCellStateListeners: DisposableStore[] = []; @@ -334,6 +333,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.useRenderer = !isWeb && !!this.configurationService.getValue(ExperimentalUseMarkdownRenderer) && !accessibilityService.isScreenReaderOptimized(); this._notebookOptions = new NotebookOptions(this.configurationService); this._register(this._notebookOptions); + this._viewContext = new ViewContext(this._notebookOptions, new NotebookEventDispatcher()); this._overlayContainer = document.createElement('div'); this.scopedContextKeyService = contextKeyService.createScoped(this._overlayContainer); @@ -367,11 +367,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._updateForNotebookConfiguration(); } - if (e.compactView || e.focusIndicator) { + if (e.compactView || e.focusIndicator || e.insertToolbarPosition) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); } + + if (this._dimension && this._isVisible) { + this.layout(this._dimension); + } })); this.notebookEditorService.addNotebookEditor(this); @@ -552,11 +556,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor codeCellLeftMargin, markdownCellBottomMargin, markdownCellTopMargin, - bottomCellToolbarGap, - bottomCellToolbarHeight, + bottomToolbarGap: bottomCellToolbarGap, + bottomToolbarHeight: bottomCellToolbarHeight, collapsedIndicatorHeight, compactView, - focusIndicator + focusIndicator, + insertToolbarPosition } = this._notebookOptions.getLayoutConfiguration(); const styleSheets: string[] = []; @@ -567,6 +572,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin}px; }`); } + // focus indicator if (focusIndicator === 'border') { styleSheets.push(` .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, @@ -645,6 +651,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }`); } + // between cell insert toolbar + if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') { + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: flex; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: flex; }`); + } else { + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: none; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: none; }`); + } + + // top insert toolbar + const topInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + styleSheets.push(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${topInsertToolbarHeight}px }`); + styleSheets.push(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, + .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { + padding-top: ${topInsertToolbarHeight}px; + box-sizing: border-box; + }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${cellRightMargin}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${cellTopMargin}px; }`); @@ -670,7 +694,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${cellBottomMargin}px; }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${codeCellLeftMargin + cellRunGutter}px; height: ${collapsedIndicatorHeight}px; }`); - styleSheets.push(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${SCROLLABLE_ELEMENT_PADDING_TOP}px }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { height: ${bottomCellToolbarHeight}px }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { height: ${bottomCellToolbarHeight}px }`); @@ -711,6 +734,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor 'NotebookCellList', this._overlayContainer, this._body, + this._viewContext, this._listDelegate, renderers, this.scopedContextKeyService, @@ -1093,8 +1117,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined) { await this._createWebview(this.getId(), textModel.uri); - - this._viewContext = new ViewContext(this._notebookOptions, new NotebookEventDispatcher()); this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo()); this._viewContext.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); @@ -1195,7 +1217,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); if (this._dimension) { - this._list.layout(this._dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, this._dimension.width); + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + this._list.layout(this._dimension.height - topInserToolbarHeight, this._dimension.width); } else { this._list.layout(); } @@ -1423,16 +1446,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }; } + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + this._dimension = new DOM.Dimension(dimension.width, dimension.height); DOM.size(this._body, dimension.width, dimension.height - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0)); - if (this._list.getRenderHeight() < dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP) { + if (this._list.getRenderHeight() < dimension.height - topInserToolbarHeight) { // the new dimension is larger than the list viewport, update its additional height first, otherwise the list view will move down a bit (as the `scrollBottom` will move down) - this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); - this._list.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); + this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - topInserToolbarHeight - 50)) : topInserToolbarHeight }); + this._list.layout(dimension.height - topInserToolbarHeight, dimension.width); } else { // the new dimension is smaller than the list viewport, if we update the additional height, the `scrollBottom` will move up, which moves the whole list view upwards a bit. So we run a layout first. - this._list.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); - this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); + this._list.layout(dimension.height - topInserToolbarHeight, dimension.width); + this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - topInserToolbarHeight - 50)) : topInserToolbarHeight }); } this._overlayContainer.style.visibility = 'visible'; @@ -2385,8 +2410,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cell = this.getCellById(cellId); const layoutConfiguration = this._notebookOptions.getLayoutConfiguration(); if (cell && cell instanceof MarkdownCellViewModel) { - if (height + layoutConfiguration.bottomCellToolbarGap !== cell.layoutInfo.totalHeight) { - this._debug('updateMarkdownCellHeight', cell.handle, height + layoutConfiguration.bottomCellToolbarGap, isInit); + if (height + layoutConfiguration.bottomToolbarGap !== cell.layoutInfo.totalHeight) { + this._debug('updateMarkdownCellHeight', cell.handle, height + layoutConfiguration.bottomToolbarGap, isInit); cell.renderedMarkdownHeight = height; } } @@ -2460,7 +2485,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webviewTransparentCover = null; this._dndController = null; this._listTopCellToolbar = null; - this._viewContext = undefined; this._notebookViewModel = undefined; this._cellContextKeyManager = null; this._renderedEditors.clear(); @@ -2606,12 +2630,6 @@ export const cellEditorBackground = registerColor('notebook.cellEditorBackground }, nls.localize('notebook.cellEditorBackground', "Cell editor background color.")); registerThemingParticipant((theme, collector) => { - collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, - .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { - padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; - box-sizing: border-box; - }`); - const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.notebookOverlay .output a, diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index f14f3fcca8d..eec47318496 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -24,8 +24,8 @@ import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/ import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { clamp } from 'vs/base/common/numbers'; -import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; import { ISplice } from 'vs/base/common/sequence'; +import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; export interface IFocusNextPreviousDelegate { onFocusNext(applyFocusNext: () => void): void; @@ -91,10 +91,13 @@ export class NotebookCellList extends WorkbenchList implements ID private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate; + private readonly _viewContext: ViewContext; + constructor( private listUser: string, parentContainer: HTMLElement, container: HTMLElement, + viewContext: ViewContext, delegate: IListVirtualDelegate, renderers: IListRenderer[], contextKeyService: IContextKeyService, @@ -106,6 +109,7 @@ export class NotebookCellList extends WorkbenchList implements ID ) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService).set(true); + this._viewContext = viewContext; this._focusNextPreviousDelegate = options.focusNextPreviousDelegate; this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { @@ -900,7 +904,8 @@ export class NotebookCellList extends WorkbenchList implements ID } getViewScrollBottom() { - return this.getViewScrollTop() + this.view.renderHeight - SCROLLABLE_ELEMENT_PADDING_TOP; + const topInsertToolbarHeight = this._viewContext.notebookOptions.computeTopInserToolbarHeight(); + return this.getViewScrollTop() + this.view.renderHeight - topInsertToolbarHeight; } private _revealRange(viewIndex: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts index fca5bd43ce6..7f9cd47a3cd 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts @@ -157,7 +157,7 @@ export class CellDragAndDropController extends Disposable { private updateInsertIndicator(dropDirection: string, insertionIndicatorAbsolutePos: number) { const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + layoutInfo.bottomCellToolbarGap / 2; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + layoutInfo.bottomToolbarGap / 2; if (insertionIndicatorTop >= 0) { this.listInsertionIndicator.style.top = `${insertionIndicatorTop}px`; this.setInsertIndicatorVisibility(true); @@ -200,7 +200,7 @@ export class CellDragAndDropController extends Disposable { const cellHeight = this.list.elementHeight(draggedOverCell); const insertionIndicatorAbsolutePos = dropDirection === 'above' ? cellTop : cellTop + cellHeight; const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + layoutInfo.bottomCellToolbarGap / 2; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + layoutInfo.bottomToolbarGap / 2; const editorHeight = this.notebookEditor.getDomNode().getBoundingClientRect().height; if (insertionIndicatorTop < 0 || insertionIndicatorTop > editorHeight) { // Ignore drop, insertion point is off-screen diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 23f94176202..555c743706c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -881,10 +881,10 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); templateData.focusIndicatorLeft.style.height = `${element.layoutInfo.indicatorHeight}px`; templateData.focusIndicatorRight.style.height = `${element.layoutInfo.indicatorHeight}px`; - templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - layoutInfo.bottomCellToolbarGap - layoutInfo.cellBottomMargin}px`; + templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - layoutInfo.bottomToolbarGap - layoutInfo.cellBottomMargin}px`; templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; templateData.outputShowMoreContainer.style.top = `${element.layoutInfo.outputShowMoreContainerOffset}px`; - templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - layoutInfo.bottomCellToolbarGap}px`; + templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - layoutInfo.bottomToolbarGap}px`; } renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 57ed3710937..c1ebfdb4c06 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -147,7 +147,7 @@ export abstract class BaseCellViewModel extends Disposable { })); this._register(this._viewContext.notebookOptions.onDidChangeOptions(e => { - if (e.cellStatusBarVisibility) { + if (e.cellStatusBarVisibility || e.insertToolbarPosition) { this.layoutChange({}); } })); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index c757fcc2671..479bb337226 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -179,8 +179,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod + editorHeight + statusbarHeight; const outputShowMoreContainerOffset = totalHeight - - notebookLayoutConfiguration.bottomCellToolbarGap - - notebookLayoutConfiguration.bottomCellToolbarHeight / 2 + - notebookLayoutConfiguration.bottomToolbarGap + - notebookLayoutConfiguration.bottomToolbarHeight / 2 - outputShowMoreContainerHeight; const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight); const editorWidth = state.outerWidth !== undefined @@ -209,11 +209,11 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod notebookLayoutConfiguration.cellTopMargin + notebookLayoutConfiguration.collapsedIndicatorHeight + notebookLayoutConfiguration.cellBottomMargin //CELL_BOTTOM_MARGIN - + notebookLayoutConfiguration.bottomCellToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + + notebookLayoutConfiguration.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + outputTotalHeight + outputShowMoreContainerHeight; const outputShowMoreContainerOffset = totalHeight - - notebookLayoutConfiguration.bottomCellToolbarGap - - notebookLayoutConfiguration.bottomCellToolbarHeight / 2 + - notebookLayoutConfiguration.bottomToolbarGap + - notebookLayoutConfiguration.bottomToolbarHeight / 2 - outputShowMoreContainerHeight; const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight); const editorWidth = state.outerWidth !== undefined @@ -314,7 +314,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod + this.getEditorStatusbarHeight() + outputsTotalHeight + outputShowMoreContainerHeight - + layoutConfiguration.bottomCellToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + + layoutConfiguration.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + layoutConfiguration.cellBottomMargin; // CELL_BOTTOM_MARGIN; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index 1703faa5359..c0b94acf079 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -29,7 +29,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie set renderedMarkdownHeight(newHeight: number) { if (this.getEditState() === CellEditState.Preview) { - const newTotalHeight = newHeight + this.viewContext.notebookOptions.getLayoutConfiguration().bottomCellToolbarGap; // BOTTOM_CELL_TOOLBAR_GAP; + const newTotalHeight = newHeight + this.viewContext.notebookOptions.getLayoutConfiguration().bottomToolbarGap; // BOTTOM_CELL_TOOLBAR_GAP; this.totalHeight = newTotalHeight; } } @@ -52,7 +52,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie this.totalHeight = this._editorHeight + layoutConfiguration.markdownCellTopMargin // MARKDOWN_CELL_TOP_MARGIN + layoutConfiguration.markdownCellBottomMargin // MARKDOWN_CELL_BOTTOM_MARGIN - + layoutConfiguration.bottomCellToolbarGap // BOTTOM_CELL_TOOLBAR_GAP + + layoutConfiguration.bottomToolbarGap // BOTTOM_CELL_TOOLBAR_GAP + this.viewContext.notebookOptions.computeStatusBarHeight(); } @@ -120,7 +120,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie editorWidth: initialNotebookLayoutInfo?.width ? this.viewContext.notebookOptions.computeMarkdownCellEditorWidth(initialNotebookLayoutInfo.width) : 0, - bottomToolbarOffset: this.viewContext.notebookOptions.getLayoutConfiguration().bottomCellToolbarGap, // BOTTOM_CELL_TOOLBAR_GAP, + bottomToolbarOffset: this.viewContext.notebookOptions.getLayoutConfiguration().bottomToolbarGap, // BOTTOM_CELL_TOOLBAR_GAP, totalHeight: 0 }; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index fc55f06bfef..f3fd40adfee 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -898,6 +898,7 @@ export const NotebookTextDiffEditorPreview = 'notebook.diff.enablePreview'; export const ExperimentalUseMarkdownRenderer = 'notebook.experimental.useMarkdownRenderer'; export const ExperimentalCompactView = 'notebook.experimental.compactView'; export const ExperimentalFocusIndicator = 'notebook.experimental.cellFocusIndicator'; +export const ExperimentalInsertToolbarPosition = 'notebook.experimental.insertToolbarPosition'; export const ExperimentalUndoRedoPerCell = 'notebook.experimental.undoRedoPerCell'; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index 7e5f68b509a..951b6b16b34 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -6,7 +6,9 @@ import { Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalFocusIndicator, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalFocusIndicator, ExperimentalInsertToolbarPosition, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +const SCROLLABLE_ELEMENT_PADDING_TOP = 18; let EDITOR_TOP_PADDING = 12; const editorTopPaddingChangeEmitter = new Emitter(); @@ -33,8 +35,8 @@ export interface NotebookLayoutConfiguration { markdownCellTopMargin: number; markdownCellBottomMargin: number; markdownPreviewPadding: number; - bottomCellToolbarGap: number; - bottomCellToolbarHeight: number; + bottomToolbarGap: number; + bottomToolbarHeight: number; editorToolbarHeight: number; editorTopPadding: number; editorBottomPadding: number; @@ -46,6 +48,7 @@ export interface NotebookLayoutConfiguration { cellToolbarInteraction: string; compactView: boolean; focusIndicator: 'border' | 'gutter'; + insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'; } interface NotebookOptionsChangeEvent { @@ -55,6 +58,7 @@ interface NotebookOptionsChangeEvent { editorTopPadding?: boolean; compactView?: boolean; focusIndicator?: boolean; + insertToolbarPosition?: boolean; } const defaultConfigConstants = { @@ -63,7 +67,6 @@ const defaultConfigConstants = { markdownCellTopMargin: 8, markdownCellBottomMargin: 8, markdownCellLeftMargin: 32, - bottomCellToolbarGap: 18, }; const compactConfigConstants = { @@ -72,7 +75,6 @@ const compactConfigConstants = { markdownCellTopMargin: 6, markdownCellBottomMargin: 6, markdownCellLeftMargin: 32, - bottomCellToolbarGap: 12, }; export class NotebookOptions { @@ -87,6 +89,8 @@ export class NotebookOptions { const cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); const compactView = this.configurationService.getValue(ExperimentalCompactView); const focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator) ?? 'border'; + const insertToolbarPosition = this.configurationService.getValue<'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'>(ExperimentalInsertToolbarPosition) ?? 'both'; + const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(compactView, insertToolbarPosition); this._disposables = []; this._layoutConfiguration = { @@ -97,7 +101,8 @@ export class NotebookOptions { cellStatusBarHeight: 22, cellOutputPadding: 14, markdownPreviewPadding: 8, - bottomCellToolbarHeight: 22, + bottomToolbarHeight: bottomToolbarHeight, + bottomToolbarGap: bottomToolbarGap, editorToolbarHeight: 0, editorTopPadding: EDITOR_TOP_PADDING, editorBottomPadding: 4, @@ -107,7 +112,8 @@ export class NotebookOptions { cellToolbarLocation, cellToolbarInteraction, compactView, - focusIndicator + focusIndicator, + insertToolbarPosition }; this._disposables.push(this.configurationService.onDidChangeConfiguration(e => { @@ -116,8 +122,9 @@ export class NotebookOptions { let cellToolbarInteraction = e.affectsConfiguration(CellToolbarVisibility); let compactView = e.affectsConfiguration(ExperimentalCompactView); let focusIndicator = e.affectsConfiguration(ExperimentalFocusIndicator); + let insertToolbarPosition = e.affectsConfiguration(ExperimentalInsertToolbarPosition); - if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator) { + if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator && !insertToolbarPosition) { return; } @@ -147,6 +154,13 @@ export class NotebookOptions { configuration.compactView = compactViewValue; } + if (insertToolbarPosition) { + configuration.insertToolbarPosition = this.configurationService.getValue<'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'>(ExperimentalInsertToolbarPosition) ?? 'both'; + const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(configuration.compactView, configuration.insertToolbarPosition); + configuration.bottomToolbarHeight = bottomToolbarHeight; + configuration.bottomToolbarGap = bottomToolbarGap; + } + this._layoutConfiguration = configuration; // trigger event @@ -155,7 +169,8 @@ export class NotebookOptions { cellToolbarLocation: cellToolbarLocation, cellToolbarInteraction: cellToolbarInteraction, compactView: compactView, - focusIndicator: focusIndicator + focusIndicator: focusIndicator, + insertToolbarPosition: insertToolbarPosition }); })); @@ -167,6 +182,23 @@ export class NotebookOptions { })); } + private _computeBottomToolbarDimensions(compactView: boolean, insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'): { bottomToolbarGap: number, bottomToolbarHeight: number } { + if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') { + return compactView ? { + bottomToolbarGap: 12, + bottomToolbarHeight: 22 + } : { + bottomToolbarGap: 18, + bottomToolbarHeight: 22 + }; + } else { + return { + bottomToolbarGap: 0, + bottomToolbarHeight: 0 + }; + } + } + getLayoutConfiguration(): NotebookLayoutConfiguration { return this._layoutConfiguration; } @@ -174,14 +206,14 @@ export class NotebookOptions { computeCollapsedMarkdownCellHeight(): number { return this._layoutConfiguration.markdownCellTopMargin + this._layoutConfiguration.collapsedIndicatorHeight - + this._layoutConfiguration.bottomCellToolbarGap + + this._layoutConfiguration.bottomToolbarGap + this._layoutConfiguration.markdownCellBottomMargin; } computeBottomToolbarOffset(totalHeight: number) { return totalHeight - - this._layoutConfiguration.bottomCellToolbarGap - - this._layoutConfiguration.bottomCellToolbarHeight / 2; + - this._layoutConfiguration.bottomToolbarGap + - this._layoutConfiguration.bottomToolbarHeight / 2; } computeCodeCellEditorWidth(outerWidth: number): number { @@ -275,11 +307,19 @@ export class NotebookOptions { computeIndicatorPosition(totalHeight: number) { return { - bottomIndicatorTop: totalHeight - this._layoutConfiguration.bottomCellToolbarGap - this._layoutConfiguration.cellBottomMargin, - verticalIndicatorHeight: totalHeight - this._layoutConfiguration.bottomCellToolbarGap + bottomIndicatorTop: totalHeight - this._layoutConfiguration.bottomToolbarGap - this._layoutConfiguration.cellBottomMargin, + verticalIndicatorHeight: totalHeight - this._layoutConfiguration.bottomToolbarGap }; } + computeTopInserToolbarHeight(): number { + if (this._layoutConfiguration.insertToolbarPosition === 'betweenCells' || this._layoutConfiguration.insertToolbarPosition === 'both') { + return SCROLLABLE_ELEMENT_PADDING_TOP; + } else { + return 0; + } + } + dispose() { this._disposables.forEach(d => d.dispose()); this._disposables = []; diff --git a/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts index 84e2e5aad25..122ffb94bc9 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('NotebookCellList', () => { const instantiationService = setupInstantiationService(); + const notebookDefaultOptions = new NotebookOptions(instantiationService.get(IConfigurationService)); + const topInsertToolbarHeight = notebookDefaultOptions.computeTopInserToolbarHeight(); test('revealElementsInView: reveal fully visible cell should not scroll', async function () { await withTestNotebook( @@ -32,7 +35,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // scroll a bit, scrollTop to bottom: 5, 215 cellList.scrollTop = 5; @@ -77,7 +80,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -116,12 +119,12 @@ suite('NotebookCellList', () => { }); const cellList = createNotebookCellList(instantiationService); - // without additionalscrollheight, the last 20 px will always be hidden due to `SCROLLABLE_ELEMENT_PADDING_TOP` + // without additionalscrollheight, the last 20 px will always be hidden due to `topInsertToolbarHeight` cellList.updateOptions({ additionalScrollHeight: 100 }); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -154,7 +157,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -196,7 +199,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -249,7 +252,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -283,7 +286,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); From 8e87fea2f15c2fbb1aa432bf9108025daaabfdbf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 12:05:55 -0700 Subject: [PATCH 143/270] Support dragging to unsplit --- .../terminal/browser/terminalTabsList.ts | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 85c332eac6c..f1845c45f5a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -471,18 +471,26 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { return; } let focused = false; - if (!targetInstance) { - // TODO: Support dropping on empty - return; - } + + let sourceInstances: ITerminalInstance[] = []; for (const e of draggedElement) { if ('instanceId' in e) { - const instance = e as ITerminalInstance; - this._terminalService.moveGroup(instance, targetInstance); - if (!focused) { - this._terminalService.setActiveInstance(instance); - focused = true; - } + sourceInstances.push(e as ITerminalInstance); + } + } + + if (!targetInstance) { + for (const instance of sourceInstances) { + this._terminalService.unsplitInstance(instance); + } + return; + } + + for (const instance of sourceInstances) { + this._terminalService.moveGroup(instance, targetInstance); + if (!focused) { + this._terminalService.setActiveInstance(instance); + focused = true; } } } From 05f83d95d2b51f6bdbeb99be47a40365d40a1e8b Mon Sep 17 00:00:00 2001 From: Nicholas Rayburn <52075362+nrayburn-tech@users.noreply.github.com> Date: Wed, 19 May 2021 14:31:42 -0500 Subject: [PATCH 144/270] Fix issue reporter not debounce submissions (#123340) --- .../electron-sandbox/issue/issueReporterMain.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 5d28a61c896..6e93aa1e4b8 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -11,6 +11,7 @@ import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; +import { Delayer } from 'vs/base/common/async'; import { groupBy } from 'vs/base/common/collections'; import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -62,6 +63,7 @@ export class IssueReporter extends Disposable { private receivedPerformanceInfo = false; private shouldQueueSearch = false; private hasBeenSubmitted = false; + private delayedSubmit = new Delayer(300); private readonly previewButton!: Button; @@ -356,7 +358,11 @@ export class IssueReporter extends Disposable { this.searchIssues(title, fileOnExtension, fileOnMarketplace); }); - this.previewButton.onDidClick(() => this.createIssue()); + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); function sendWorkbenchCommand(commandId: string) { ipcRenderer.send('vscode:workbenchCommand', { id: commandId, from: 'issueReporter' }); @@ -383,9 +389,11 @@ export class IssueReporter extends Disposable { const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; // Cmd/Ctrl+Enter previews issue and closes window if (cmdOrCtrlKey && e.keyCode === 13) { - if (await this.createIssue()) { - ipcRenderer.send('vscode:closeIssueReporter'); - } + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + ipcRenderer.send('vscode:closeIssueReporter'); + } + }); } // Cmd/Ctrl + w closes issue window From c13c6d8e468dfda249f1f1a1f755f23ed9b46993 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 13 May 2021 11:27:17 -0700 Subject: [PATCH 145/270] Throw error when trying to reload/navigate within a webview This causes the webview document to get into an invalid state and is currently not supported. This new error message should make the issue more clear --- src/vs/workbench/contrib/webview/browser/pre/host.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/host.js b/src/vs/workbench/contrib/webview/browser/pre/host.js index 8310f417ca0..7e773adefab 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/host.js +++ b/src/vs/workbench/contrib/webview/browser/pre/host.js @@ -6,8 +6,13 @@ import { createWebviewManager } from './main.js'; -const id = document.location.search.match(/\bid=([\w-]+)/)[1]; -const onElectron = /platform=electron/.test(document.location.search); +const searchParams = new URL(location.toString()).searchParams; +const id = searchParams.get('id'); +if (!id) { + throw new Error('Could not resolve webview id. Webview will not work.\nThis is usually caused by incorrectly trying to navigate in a webview'); +} + +const onElectron = searchParams.get('platform') === 'electron'; const hostMessaging = new class HostMessaging { constructor() { From c63ac2f638804572a6062f79899be7567037813c Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 13 May 2021 11:59:09 -0700 Subject: [PATCH 146/270] Use searchParams to get id --- .../contrib/webview/browser/pre/service-worker.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index e61fc59ad84..4a2d6b97462 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -318,16 +318,24 @@ async function processLocalhostRequest(event, requestUrl) { return promise.then(resolveRedirect); } +/** + * @param {Client} client + * @returns {string | undefined} + */ function getWebviewIdForClient(client) { const requesterClientUrl = new URL(client.url); - return requesterClientUrl.search.match(/\bid=([a-z0-9-]+)/i)[1]; + return requesterClientUrl.searchParams.get('id'); } +/** + * @param {string} webviewId + * @returns {Promise} + */ async function getOuterIframeClient(webviewId) { const allClients = await sw.clients.matchAll({ includeUncontrolled: true }); return allClients.find(client => { const clientUrl = new URL(client.url); const hasExpectedPathName = (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html` || clientUrl.pathname === `${rootPath}/electron-browser-index.html`); - return hasExpectedPathName && clientUrl.search.match(new RegExp('\\bid=' + webviewId)); + return hasExpectedPathName && clientUrl.searchParams.get('id') === webviewId; }); } From f0ef8dfd913cd948db616266423f14e284384520 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 13 May 2021 12:11:50 -0700 Subject: [PATCH 147/270] Add explicit null typings --- .../webview/browser/pre/service-worker.js | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 4a2d6b97462..b5f56258799 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -66,9 +66,15 @@ class RequestStore { create() { const requestId = ++this.requestPool; + /** @type {undefined | ((x: T) => void)} */ let resolve; + + /** @type {Promise} */ const promise = new Promise(r => resolve = r); - const entry = { resolve, promise }; + + /** @type {RequestStoreEntry} */ + const entry = { resolve: /** @type {(x: T) => void} */ (resolve), promise }; + this.map.set(requestId, entry); const dispose = () => { @@ -205,11 +211,16 @@ sw.addEventListener('activate', (event) => { async function processResourceRequest(event, requestUrl) { const client = await sw.clients.get(event.clientId); if (!client) { - console.log('Could not find inner client for request'); + console.error('Could not find inner client for request'); return notFound(); } const webviewId = getWebviewIdForClient(client); + if (!webviewId) { + console.error('Could not resolve webview id'); + return notFound(); + } + const resourcePath = requestUrl.pathname.startsWith(resourceRoot + '/') ? requestUrl.pathname.slice(resourceRoot.length) : requestUrl.pathname; /** @@ -229,18 +240,18 @@ async function processResourceRequest(event, requestUrl) { } } - const cacheHeaders = entry.etag ? { - 'ETag': entry.etag, - 'Cache-Control': 'no-cache' - } : {}; - + /** @type {Record} */ + const headers = { + 'Content-Type': entry.mime, + 'Access-Control-Allow-Origin': '*', + }; + if (entry.etag) { + headers['ETag'] = entry.etag; + headers['Cache-Control'] = 'no-cache'; + } const response = new Response(entry.body, { status: 200, - headers: { - 'Content-Type': entry.mime, - 'Access-Control-Allow-Origin': '*', - ...cacheHeaders - } + headers }); if (entry.etag) { @@ -273,8 +284,9 @@ async function processResourceRequest(event, requestUrl) { } /** - * @param {*} event + * @param {FetchEvent} event * @param {URL} requestUrl + * @return {Promise} */ async function processLocalhostRequest(event, requestUrl) { const client = await sw.clients.get(event.clientId); @@ -287,9 +299,10 @@ async function processLocalhostRequest(event, requestUrl) { const origin = requestUrl.origin; /** - * @param {string} redirectOrigin + * @param {string | undefined} redirectOrigin + * @return {Promise} */ - const resolveRedirect = (redirectOrigin) => { + const resolveRedirect = async (redirectOrigin) => { if (!redirectOrigin) { return fetch(event.request); } @@ -320,7 +333,7 @@ async function processLocalhostRequest(event, requestUrl) { /** * @param {Client} client - * @returns {string | undefined} + * @returns {string | null} */ function getWebviewIdForClient(client) { const requesterClientUrl = new URL(client.url); From b8a0123cfc9a9330374904b8f3bbcfdbb270ebe6 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 13 May 2021 12:12:31 -0700 Subject: [PATCH 148/270] Fallback to fetch if localhost resolve fails --- .../contrib/webview/browser/pre/service-worker.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index b5f56258799..20576472cc5 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -162,7 +162,6 @@ sw.addEventListener('message', async (event) => { } case 'did-load-localhost': { - const webviewId = getWebviewIdForClient(event.source); const data = event.data.data; if (!localhostRequestStore.resolve(data.id, data.location)) { console.log('Could not resolve unknown localhost', data.origin); @@ -293,9 +292,14 @@ async function processLocalhostRequest(event, requestUrl) { if (!client) { // This is expected when requesting resources on other localhost ports // that are not spawned by vs code - return undefined; + return fetch(event.request); } const webviewId = getWebviewIdForClient(client); + if (!webviewId) { + console.error('Could not resolve webview id'); + return fetch(event.request); + } + const origin = requestUrl.origin; /** From 3c4176656435001add7614a8ab890a618c33e431 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 13 May 2021 12:46:17 -0700 Subject: [PATCH 149/270] Fix strict null errors in webview main --- .../contrib/webview/browser/pre/main.js | 114 +++++++++++++----- 1 file changed, 83 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index a31f57f26ce..098abfbe8a1 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check +/// + /** * @typedef {{ * postMessage: (channel: string, data?: any) => void, - * onMessage: (channel: string, handler: any) => void, + * onMessage: (channel: string, handler: (event: MessageEvent, data: any) => void) => void, * focusIframeOnCreate?: boolean, * ready?: Promise, * onIframeLoaded?: (iframe: HTMLIFrameElement) => void, @@ -56,6 +58,18 @@ const getPendingFrame = () => { return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame')); }; +/** + * @template T + * @param {T | undefined | null} obj + * @return {T} + */ +function assertIsDefined(obj) { + if (typeof obj === 'undefined' || obj === null) { + throw new Error('Found unexpected null'); + } + return obj; +} + const vscodePostMessageFuncName = '__vscode_post_message__'; const defaultStyles = document.createElement('style'); @@ -202,6 +216,9 @@ const workerReady = new Promise(async (resolve, reject) => { async registration => { await navigator.serviceWorker.ready; + /** + * @param {MessageEvent} event + */ const versionHandler = (event) => { if (event.data.channel !== 'version') { return; @@ -218,7 +235,7 @@ const workerReady = new Promise(async (resolve, reject) => { } }; navigator.serviceWorker.addEventListener('message', versionHandler); - registration.active.postMessage({ channel: 'version' }); + assertIsDefined(registration.active).postMessage({ channel: 'version' }); }, error => { reject(new Error(`Could not register service workers: ${error}.`)); @@ -231,6 +248,7 @@ const workerReady = new Promise(async (resolve, reject) => { export async function createWebviewManager(host) { // state let firstLoad = true; + /** @type {any} */ let loadTimeout; let styleVersion = 0; @@ -241,7 +259,7 @@ export async function createWebviewManager(host) { /** @type {number | undefined} */ initialScrollProgress: undefined, - /** @type {{ [key: string]: string }} */ + /** @type {{ [key: string]: string } | undefined} */ styles: undefined, /** @type {string | undefined} */ @@ -253,13 +271,13 @@ export async function createWebviewManager(host) { host.onMessage('did-load-resource', (_event, data) => { navigator.serviceWorker.ready.then(registration => { - registration.active.postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); + assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); }); }); host.onMessage('did-load-localhost', (_event, data) => { navigator.serviceWorker.ready.then(registration => { - registration.active.postMessage({ channel: 'did-load-localhost', data }); + assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data }); }); }); @@ -282,7 +300,9 @@ export async function createWebviewManager(host) { if (body) { body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast'); - body.classList.add(initData.activeTheme); + if (initData.activeTheme) { + body.classList.add(initData.activeTheme); + } body.dataset.vscodeThemeKind = initData.activeTheme; body.dataset.vscodeThemeName = initData.themeName || ''; @@ -435,6 +455,9 @@ export async function createWebviewManager(host) { let isHandlingScroll = false; + /** + * @param {WheelEvent} event + */ const handleWheel = (event) => { if (isHandlingScroll) { return; @@ -450,15 +473,21 @@ export async function createWebviewManager(host) { }); }; + /** + * @param {Event} event + */ const handleInnerScroll = (event) => { - if (!event.target || !event.target.body) { - return; - } if (isHandlingScroll) { return; } - const progress = event.currentTarget.scrollY / event.target.body.clientHeight; + const target = /** @type {HTMLDocument | null} */ (event.target); + const currentTarget = /** @type {Window | null} */ (event.currentTarget); + if (!target || !currentTarget || !target.body) { + return; + } + + const progress = currentTarget.scrollY / target.body.clientHeight; if (isNaN(progress)) { return; } @@ -475,6 +504,19 @@ export async function createWebviewManager(host) { }; /** + * @typedef {{ + * contents: string; + * options: { + * readonly allowScripts: boolean; + * readonly allowMultipleAPIAcquire: boolean; + * } + * state: any; + * resourceEndpoint: string; + * }} ContentUpdateData + */ + + /** + * @param {ContentUpdateData} data * @return {string} */ function toContentHtml(data) { @@ -484,7 +526,10 @@ export async function createWebviewManager(host) { newDocument.querySelectorAll('a').forEach(a => { if (!a.title) { - a.title = a.getAttribute('href'); + const href = a.getAttribute('href'); + if (typeof href === 'string') { + a.title = href; + } } }); @@ -509,8 +554,11 @@ export async function createWebviewManager(host) { try { // Attempt to rewrite CSPs that hardcode old-style resource endpoint const endpointUrl = new URL(data.resourceEndpoint); - const newCsp = csp.getAttribute('content').replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); - csp.setAttribute('content', newCsp); + const cspContent = csp.getAttribute('content'); + if (cspContent) { + const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); + csp.setAttribute('content', newCsp); + } } catch (e) { console.error(`Could not rewrite csp: ${e}`); } @@ -563,7 +611,7 @@ export async function createWebviewManager(host) { // update iframe-contents let updateId = 0; - host.onMessage('content', async (_event, data) => { + host.onMessage('content', async (_event, /** @type {ContentUpdateData} */ data) => { const currentUpdateId = ++updateId; try { @@ -586,18 +634,19 @@ export async function createWebviewManager(host) { const frame = getActiveFrame(); const wasFirstLoad = firstLoad; // keep current scrollY around and use later + /** @type {(body: HTMLElement, window: Window) => void} */ let setInitialScrollPosition; if (firstLoad) { firstLoad = false; setInitialScrollPosition = (body, window) => { - if (!isNaN(initData.initialScrollProgress)) { + if (typeof initData.initialScrollProgress === 'number' && !isNaN(initData.initialScrollProgress)) { if (window.scrollY === 0) { window.scroll(0, body.clientHeight * initData.initialScrollProgress); } } }; } else { - const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentWindow.scrollY : 0; + const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? assertIsDefined(frame.contentWindow).scrollY : 0; setInitialScrollPosition = (body, window) => { if (window.scrollY === 0) { window.scroll(0, scrollY); @@ -656,15 +705,16 @@ export async function createWebviewManager(host) { return; } - if (newFrame.contentDocument.readyState !== 'loading') { + const contentDocument = assertIsDefined(newFrame.contentDocument); + if (contentDocument.readyState !== 'loading') { clearInterval(interval); - onFrameLoaded(newFrame.contentDocument); + onFrameLoaded(contentDocument); } }, 10); } else { - newFrame.contentWindow.addEventListener('DOMContentLoaded', e => { + assertIsDefined(newFrame.contentWindow).addEventListener('DOMContentLoaded', e => { const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; - onFrameLoaded(contentDocument); + onFrameLoaded(assertIsDefined(contentDocument)); }); } @@ -692,7 +742,7 @@ export async function createWebviewManager(host) { newFrame.setAttribute('id', 'active-frame'); newFrame.style.visibility = 'visible'; if (host.focusIframeOnCreate) { - newFrame.contentWindow.focus(); + assertIsDefined(newFrame.contentWindow).focus(); } contentWindow.addEventListener('scroll', handleInnerScroll); @@ -720,10 +770,12 @@ export async function createWebviewManager(host) { loadTimeout = setTimeout(() => { clearTimeout(loadTimeout); loadTimeout = undefined; - onLoad(newFrame.contentDocument, newFrame.contentWindow); + onLoad(assertIsDefined(newFrame.contentDocument), assertIsDefined(newFrame.contentWindow)); }, 200); - newFrame.contentWindow.addEventListener('load', function (e) { + const contentWindow = assertIsDefined(newFrame.contentWindow); + + contentWindow.addEventListener('load', function (e) { const contentDocument = /** @type {Document} */ (e.target); if (loadTimeout) { @@ -734,11 +786,11 @@ export async function createWebviewManager(host) { }); // Bubble out various events - newFrame.contentWindow.addEventListener('click', handleInnerClick); - newFrame.contentWindow.addEventListener('auxclick', handleAuxClick); - newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); - newFrame.contentWindow.addEventListener('keyup', handleInnerUp); - newFrame.contentWindow.addEventListener('contextmenu', e => { + contentWindow.addEventListener('click', handleInnerClick); + contentWindow.addEventListener('auxclick', handleAuxClick); + contentWindow.addEventListener('keydown', handleInnerKeydown); + contentWindow.addEventListener('keyup', handleInnerUp); + contentWindow.addEventListener('contextmenu', e => { e.preventDefault(); host.postMessage('did-context-menu', { clientX: e.clientX, @@ -760,7 +812,7 @@ export async function createWebviewManager(host) { if (!pending) { const target = getActiveFrame(); if (target) { - target.contentWindow.postMessage(data.message, '*', data.transfer); + assertIsDefined(target.contentWindow).postMessage(data.message, '*', data.transfer); return; } } @@ -776,7 +828,7 @@ export async function createWebviewManager(host) { if (!target) { return; } - target.contentDocument.execCommand(data); + assertIsDefined(target.contentDocument).execCommand(data); }); trackFocus({ @@ -784,7 +836,7 @@ export async function createWebviewManager(host) { onBlur: () => host.postMessage('did-blur') }); - (/** @type {any} */ (window))[vscodePostMessageFuncName] = (command, data, transfer) => { + (/** @type {any} */ (window))[vscodePostMessageFuncName] = (/** @type {string} */ command, /** @type {any} */ data) => { switch (command) { case 'onmessage': case 'do-update-state': From 40a268505754cc0ff4b7a9e514ccce57b2dc433e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 13 May 2021 13:38:20 -0700 Subject: [PATCH 150/270] Fix shift for markdown cell selection only selecting current cell --- .../notebook/browser/notebookBrowser.ts | 2 +- .../notebook/browser/notebookEditorWidget.ts | 27 ++++++++++++++++--- .../view/renderers/backLayerWebView.ts | 4 +-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index d188dea2b16..6d4bf6077d5 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -170,7 +170,7 @@ export interface ICommonNotebookEditor { triggerScroll(event: IMouseWheelEvent): void; getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; getCellById(cellId: string): IGenericCellViewModel | undefined; - toggleNotebookCellSelection(cell: IGenericCellViewModel): void; + toggleNotebookCellSelection(cell: IGenericCellViewModel, selectFromPrevious: boolean): void; focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): void; focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean, source?: string): void; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 2307875a22b..c588c2a5290 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1953,19 +1953,38 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - toggleNotebookCellSelection(cell: ICellViewModel): void { + toggleNotebookCellSelection(selectedCell: ICellViewModel, selectFromPrevious: boolean): void { const currentSelections = this._list.getSelectedElements(); + const isSelected = currentSelections.includes(selectedCell); - const isSelected = currentSelections.includes(cell); + const previousSelection = selectFromPrevious ? currentSelections[currentSelections.length - 1] ?? selectedCell : selectedCell; + const selectedIndex = this._list.getViewIndex(selectedCell)!; + const previousIndex = this._list.getViewIndex(previousSelection)!; + + const cellsInSelectionRange = this.getCellsInRange(selectedIndex, previousIndex); if (isSelected) { // Deselect - this._list.selectElements(currentSelections.filter(current => current !== cell)); + this._list.selectElements(currentSelections.filter(current => !cellsInSelectionRange.includes(current))); } else { // Add to selection - this._list.selectElements([...currentSelections, cell]); + this.focusElement(selectedCell); + this._list.selectElements([...currentSelections.filter(current => !cellsInSelectionRange.includes(current)), ...cellsInSelectionRange]); } } + private getCellsInRange(fromInclusive: number, toInclusive: number): ICellViewModel[] { + const selectedCellsInRange: ICellViewModel[] = []; + for (let index = 0; index < this._list.length; ++index) { + const cell = this._list.element(index); + if (cell) { + if ((index >= fromInclusive && index <= toInclusive) || (index >= toInclusive && index <= fromInclusive)) { + selectedCellsInRange.push(cell); + } + } + } + return selectedCellsInRange; + } + focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) { if (this._isDisposed) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 0a0ce408a70..1c6a07379d6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -1025,8 +1025,8 @@ var requirejs = (function() { const cell = this.notebookEditor.getCellById(data.cellId); if (cell) { if (data.shiftKey || (isMacintosh ? data.metaKey : data.ctrlKey)) { - // Add to selection - this.notebookEditor.toggleNotebookCellSelection(cell); + // Modify selection + this.notebookEditor.toggleNotebookCellSelection(cell, /* fromPrevious */ data.shiftKey); } else { // Normal click this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); From de1c8ad93a9842d2fab47b541dd5ed63fdd19b1e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 13 May 2021 13:53:05 -0700 Subject: [PATCH 151/270] Give clearer names and add comment --- .../browser/view/renderers/cellDnd.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts index fca5bd43ce6..a3cdafe311d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts @@ -344,16 +344,19 @@ export class CellDragAndDropController extends Disposable { return; } - const viewRect = this.notebookEditor.getDomNode().getBoundingClientRect(); + const notebookViewRect = this.notebookEditor.getDomNode().getBoundingClientRect(); const eventPositionInView = position.clientY - this.list.scrollTop; - const scrollMargin = 0.2; - const maxScrollPerFrame = 20; - const eventPositionRatio = eventPositionInView / viewRect.height; - if (eventPositionRatio < scrollMargin) { - this.list.scrollTop -= maxScrollPerFrame * (1 - eventPositionRatio / scrollMargin); - } else if (eventPositionRatio > 1 - scrollMargin) { - this.list.scrollTop += maxScrollPerFrame * (1 - ((1 - eventPositionRatio) / scrollMargin)); + // Percentage from the top/bottom of the screen where we start scrolling while dragging + const notebookViewScrollMargins = 0.2; + + const maxScrollDeltaPerFrame = 20; + + const eventPositionRatio = eventPositionInView / notebookViewRect.height; + if (eventPositionRatio < notebookViewScrollMargins) { + this.list.scrollTop -= maxScrollDeltaPerFrame * (1 - eventPositionRatio / notebookViewScrollMargins); + } else if (eventPositionRatio > 1 - notebookViewScrollMargins) { + this.list.scrollTop += maxScrollDeltaPerFrame * (1 - ((1 - eventPositionRatio) / notebookViewScrollMargins)); } } From d7f6d7e735d46b6e0a4a9a4c1c3da36b1ac0c8b9 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 18 May 2021 21:03:55 -0700 Subject: [PATCH 152/270] Fix markdown cell drag indiciator having wrong position in scrolled documents --- .../browser/diff/notebookTextDiffEditor.ts | 4 ++-- .../notebook/browser/notebookBrowser.ts | 6 +++--- .../notebook/browser/notebookEditorWidget.ts | 12 +++++------ .../view/renderers/backLayerWebView.ts | 18 ++++++---------- .../browser/view/renderers/cellDnd.ts | 21 +++++++++---------- .../browser/view/renderers/webviewPreloads.ts | 6 +++--- 6 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 9db388ec10e..75a9258c0ac 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -143,10 +143,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD setMarkdownCellEditState(cellId: string, editState: CellEditState): void { // throw new Error('Method not implemented.'); } - markdownCellDragStart(cellId: string, position: { clientY: number }): void { + markdownCellDragStart(cellId: string, event: { dragOffsetY: number }): void { // throw new Error('Method not implemented.'); } - markdownCellDrag(cellId: string, position: { clientY: number }): void { + markdownCellDrag(cellId: string, event: { dragOffsetY: number }): void { // throw new Error('Method not implemented.'); } markdownCellDragEnd(cellId: string): void { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 6d4bf6077d5..b4dbbc4020b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -177,9 +177,9 @@ export interface ICommonNotebookEditor { scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void; updateMarkdownCellHeight(cellId: string, height: number, isInit: boolean): void; setMarkdownCellEditState(cellId: string, editState: CellEditState): void; - markdownCellDragStart(cellId: string, position: { clientY: number }): void; - markdownCellDrag(cellId: string, position: { clientY: number }): void; - markdownCellDrop(cellId: string, position: { clientY: number, ctrlKey: boolean, altKey: boolean }): void; + markdownCellDragStart(cellId: string, event: { dragOffsetY: number }): void; + markdownCellDrag(cellId: string, event: { dragOffsetY: number }): void; + markdownCellDrop(cellId: string, event: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean }): void; markdownCellDragEnd(cellId: string): void; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index c588c2a5290..4fcf323a763 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2348,24 +2348,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - markdownCellDragStart(cellId: string, ctx: { clientY: number }): void { + markdownCellDragStart(cellId: string, event: { dragOffsetY: number }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.startExplicitDrag(cell, ctx); + this._dndController?.startExplicitDrag(cell, event.dragOffsetY); } } - markdownCellDrag(cellId: string, ctx: { clientY: number }): void { + markdownCellDrag(cellId: string, event: { dragOffsetY: number }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.explicitDrag(cell, ctx); + this._dndController?.explicitDrag(cell, event.dragOffsetY); } } - markdownCellDrop(cellId: string, ctx: { clientY: number, ctrlKey: boolean, altKey: boolean }): void { + markdownCellDrop(cellId: string, event: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.explicitDrop(cell, ctx); + this._dndController?.explicitDrop(cell, event); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 1c6a07379d6..44fca9b4982 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -131,17 +131,13 @@ export interface IToggleMarkdownPreviewMessage extends BaseToWebviewMessage { export interface ICellDragStartMessage extends BaseToWebviewMessage { type: 'cell-drag-start'; readonly cellId: string; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDragMessage extends BaseToWebviewMessage { type: 'cell-drag'; readonly cellId: string; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDropMessage extends BaseToWebviewMessage { @@ -149,9 +145,7 @@ export interface ICellDropMessage extends BaseToWebviewMessage { readonly cellId: string; readonly ctrlKey: boolean readonly altKey: boolean; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDragEndMessage extends BaseToWebviewMessage { @@ -1086,18 +1080,18 @@ var requirejs = (function() { } case 'cell-drag-start': { - this.notebookEditor.markdownCellDragStart(data.cellId, data.position); + this.notebookEditor.markdownCellDragStart(data.cellId, data); break; } case 'cell-drag': { - this.notebookEditor.markdownCellDrag(data.cellId, data.position); + this.notebookEditor.markdownCellDrag(data.cellId, data); break; } case 'cell-drop': { this.notebookEditor.markdownCellDrop(data.cellId, { - clientY: data.position.clientY, + dragOffsetY: data.dragOffsetY, ctrlKey: data.ctrlKey, altKey: data.altKey, }); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts index a3cdafe311d..5f35d9efb21 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts @@ -195,7 +195,7 @@ export class CellDragAndDropController extends Disposable { } } - private _dropImpl(draggedCell: ICellViewModel, dropDirection: 'above' | 'below', ctx: { clientY: number, ctrlKey: boolean, altKey: boolean }, draggedOverCell: ICellViewModel) { + private _dropImpl(draggedCell: ICellViewModel, dropDirection: 'above' | 'below', ctx: { ctrlKey: boolean, altKey: boolean }, draggedOverCell: ICellViewModel) { const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); const cellHeight = this.list.elementHeight(draggedOverCell); const insertionIndicatorAbsolutePos = dropDirection === 'above' ? cellTop : cellTop + cellHeight; @@ -323,18 +323,18 @@ export class CellDragAndDropController extends Disposable { })); } - public startExplicitDrag(cell: ICellViewModel, position: { clientY: number }) { + public startExplicitDrag(cell: ICellViewModel, _dragOffsetY: number) { this.currentDraggedCell = cell; this.setInsertIndicatorVisibility(true); } - public explicitDrag(cell: ICellViewModel, position: { clientY: number }) { - const target = this.list.elementAt(position.clientY); + public explicitDrag(cell: ICellViewModel, dragOffsetY: number) { + const target = this.list.elementAt(dragOffsetY); if (target && target !== cell) { const cellTop = this.list.getAbsoluteTopOfElement(target); const cellHeight = this.list.elementHeight(target); - const dropDirection = this.getExplicitDragDropDirection(position.clientY, cellTop, cellHeight); + const dropDirection = this.getExplicitDragDropDirection(dragOffsetY, cellTop, cellHeight); const insertionIndicatorAbsolutePos = dropDirection === 'above' ? cellTop : cellTop + cellHeight; this.updateInsertIndicator(dropDirection, insertionIndicatorAbsolutePos); } @@ -345,7 +345,7 @@ export class CellDragAndDropController extends Disposable { } const notebookViewRect = this.notebookEditor.getDomNode().getBoundingClientRect(); - const eventPositionInView = position.clientY - this.list.scrollTop; + const eventPositionInView = dragOffsetY - this.list.scrollTop; // Percentage from the top/bottom of the screen where we start scrolling while dragging const notebookViewScrollMargins = 0.2; @@ -364,24 +364,23 @@ export class CellDragAndDropController extends Disposable { this.setInsertIndicatorVisibility(false); } - public explicitDrop(cell: ICellViewModel, ctx: { clientY: number, ctrlKey: boolean, altKey: boolean }) { + public explicitDrop(cell: ICellViewModel, ctx: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean }) { this.currentDraggedCell = undefined; this.setInsertIndicatorVisibility(false); - const target = this.list.elementAt(ctx.clientY); + const target = this.list.elementAt(ctx.dragOffsetY); if (!target || target === cell) { return; } const cellTop = this.list.getAbsoluteTopOfElement(target); const cellHeight = this.list.elementHeight(target); - const dropDirection = this.getExplicitDragDropDirection(ctx.clientY, cellTop, cellHeight); + const dropDirection = this.getExplicitDragDropDirection(ctx.dragOffsetY, cellTop, cellHeight); this._dropImpl(cell, dropDirection, ctx, target); } private getExplicitDragDropDirection(clientY: number, cellTop: number, cellHeight: number) { - const dragOffset = this.list.scrollTop + clientY; - const dragPosInElement = dragOffset - cellTop; + const dragPosInElement = clientY - cellTop; const dragPosRatio = dragPosInElement / cellHeight; return this.getDropInsertDirection(dragPosRatio); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 282608bfea7..60003df8498 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1025,7 +1025,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv cellId: drag.cellId, ctrlKey: e.ctrlKey, altKey: e.altKey, - position: { clientY: e.clientY }, + dragOffsetY: e.clientY, }); }); } @@ -1041,7 +1041,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv postNotebookMessage('cell-drag-start', { cellId: cellId, - position: { clientY: e.clientY }, + dragOffsetY: e.clientY, }); // Continuously send updates while dragging instead of relying on `updateDrag`. @@ -1053,7 +1053,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv postNotebookMessage('cell-drag', { cellId: cellId, - position: { clientY: this.currentDrag.clientY }, + dragOffsetY: this.currentDrag.clientY, }); requestAnimationFrame(trySendDragUpdate); }; From a3e72ce1d068f2575f1c71d22b7329cc33237d0c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 12:38:48 -0700 Subject: [PATCH 153/270] Reinit dnd observer after reattach --- .../terminal/browser/terminalInstance.ts | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 2708a3e685c..54393822d82 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -9,7 +9,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { debounce } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -133,6 +133,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _webglAddon: WebglAddon | undefined; private _commandTrackerAddon: CommandTrackerAddon | undefined; private _navigationModeAddon: INavigationMode & ITerminalAddon | undefined; + private _dndObserver: IDisposable | undefined; private _lastLayoutDimensions: dom.Dimension | undefined; @@ -276,6 +277,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._initDimensions(); this._createProcessManager(); + + this._register(toDisposable(() => this._dndObserver?.dispose())); + this._containerReadyBarrier = new AutoOpenBarrier(Constants.WaitForContainerThreshold); this._xtermReadyPromise = this._createXterm(); this._xtermReadyPromise.then(async () => { @@ -309,13 +313,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { initialDataEventsTimeout = undefined; this._initialDataEvents = undefined; }, 10000); - this._register({ - dispose: () => { - if (initialDataEventsTimeout) { - window.clearTimeout(initialDataEventsTimeout); - } + this._register(toDisposable(() => { + if (initialDataEventsTimeout) { + window.clearTimeout(initialDataEventsTimeout); } - }); + })); } private _getIcon(): Codicon | undefined { @@ -605,6 +607,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._container?.removeChild(this._wrapperElement); this._container = container; this._container.appendChild(this._wrapperElement); + console.log('container', container); + setTimeout(() => this._initDragAndDrop(container)); } private async _attachToElement(container: HTMLElement): Promise { @@ -752,14 +756,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._refreshSelectionContextKey(); })); - const dndController = new TerminalInstanceDropAndDropController(this._container.parentElement!); - dndController.onDropTerminal(e => this._onRequestAddInstanceToGroup.fire(e)); - dndController.onDropFile(async path => { - const preparedPath = await this._terminalInstanceService.preparePathForTerminalAsync(path, this.shellLaunchConfig.executable, this.title, this.shellType, this.isRemote); - this.sendText(preparedPath, false); - this.focus(); - }); - this._register(new DragAndDropObserver(this._container.parentElement!, dndController)); + this._initDragAndDrop(container); this._widgetManager.attachToElement(xterm.element); this._processManager.onProcessReady(() => this._linkManager?.setWidgetManager(this._widgetManager)); @@ -781,6 +778,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + private _initDragAndDrop(container: HTMLElement) { + this._dndObserver?.dispose(); + const dndController = new TerminalInstanceDropAndDropController(container); + dndController.onDropTerminal(e => this._onRequestAddInstanceToGroup.fire(e)); + dndController.onDropFile(async path => { + const preparedPath = await this._terminalInstanceService.preparePathForTerminalAsync(path, this.shellLaunchConfig.executable, this.title, this.shellType, this.isRemote); + this.sendText(preparedPath, false); + this.focus(); + }); + this._dndObserver = new DragAndDropObserver(container.parentElement!, dndController); + } + private async _measureRenderTime(): Promise { await this._xtermReadyPromise; const frameTimes: number[] = []; @@ -1824,7 +1833,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } -class TerminalInstanceDropAndDropController implements IDragAndDropObserverCallbacks { +class TerminalInstanceDropAndDropController extends Disposable implements IDragAndDropObserverCallbacks { private _dropOverlay?: HTMLElement; @@ -1836,6 +1845,15 @@ class TerminalInstanceDropAndDropController implements IDragAndDropObserverCallb constructor( private readonly _container: HTMLElement ) { + super(); + this._register(toDisposable(() => this._clearDropOverlay())); + } + + private _clearDropOverlay() { + if (this._dropOverlay && this._dropOverlay.parentElement) { + this._dropOverlay.parentElement.removeChild(this._dropOverlay); + } + this._dropOverlay = undefined; } onDragEnter(e: DragEvent) { @@ -1858,17 +1876,11 @@ class TerminalInstanceDropAndDropController implements IDragAndDropObserverCallb } } onDragLeave(e: DragEvent) { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } - this._dropOverlay = undefined; + this._clearDropOverlay(); } onDragEnd(e: DragEvent) { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } - this._dropOverlay = undefined; + this._clearDropOverlay(); } onDragOver(e: DragEvent) { @@ -1880,7 +1892,6 @@ class TerminalInstanceDropAndDropController implements IDragAndDropObserverCallb // Dragging terminals if (types.includes('terminals')) { - console.log('side', this._getDropSide(e)); const side = this._getDropSide(e); this._dropOverlay.classList.toggle('drop-left', side === 'left'); this._dropOverlay.classList.toggle('drop-right', side === 'right'); @@ -1890,10 +1901,7 @@ class TerminalInstanceDropAndDropController implements IDragAndDropObserverCallb } async onDrop(e: DragEvent) { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } - this._dropOverlay = undefined; + this._clearDropOverlay(); if (!e.dataTransfer) { return; From b79825e7c4e6cb54459c32980c1e5f0d5528f2bd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 12:43:21 -0700 Subject: [PATCH 154/270] Clean up --- src/vs/workbench/contrib/terminal/browser/terminalInstance.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 54393822d82..e27b90e029e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -607,7 +607,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._container?.removeChild(this._wrapperElement); this._container = container; this._container.appendChild(this._wrapperElement); - console.log('container', container); setTimeout(() => this._initDragAndDrop(container)); } From 15f772fae328e41554613bdbfcd8f2478fe58937 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 19 May 2021 12:53:36 -0700 Subject: [PATCH 155/270] notebook: include script url in back compat patch --- .../notebook/browser/view/renderers/webviewPreloads.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index e1df7bf0485..f70fb7c26c7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -164,16 +164,17 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend if (isModule) { return __import(url); } else { - return createBackCompatModule(rendererId, text); + return createBackCompatModule(rendererId, url, text); } }; - const createBackCompatModule = (rendererId: string, scriptText: string): ScriptModule => ({ + const createBackCompatModule = (rendererId: string, scriptUrl: string, scriptText: string): ScriptModule => ({ activate: (): RendererApi => { const onDidCreateOutput = createEmitter(); const onWillDestroyOutput = createEmitter(); const globals = { + scriptUrl, acquireNotebookRendererApi: (): GlobalNotebookRendererApi => ({ onDidCreateOutput: onDidCreateOutput.event, onWillDestroyOutput: onWillDestroyOutput.event, From 25bdb01848393d4974a89a3b683fac5dd64aa8d5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 12:58:05 -0700 Subject: [PATCH 156/270] Add theme key for terminal dnd --- .../contrib/terminal/browser/media/terminal.css | 1 - .../workbench/contrib/terminal/browser/terminalView.ts | 10 ++++++++-- .../contrib/terminal/common/terminalColorRegistry.ts | 7 ++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 978d0b63529..1b1291a4c67 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -291,7 +291,6 @@ } .monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay { - background: rgba(128, 128, 255, .5); display: block; position: absolute; left: 0; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 6bcd9c5aa5e..09055f9d4f6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -12,7 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, IColorTheme, registerThemingParticipant, ICssStyleCollector, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { switchTerminalActionViewItemSeparator, switchTerminalShowTabsTitle } from 'vs/workbench/contrib/terminal/browser/terminalActions'; -import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR, TERMINAL_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; @@ -20,7 +20,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalSettingId, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; @@ -296,6 +296,12 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = collector.addRule(`.monaco-workbench .pane-body.integrated-terminal .split-view-view:not(:first-child) { border-color: ${borderColor.toString()}; }`); collector.addRule(`.monaco-workbench .pane-body.integrated-terminal .tabs-container { border-color: ${borderColor.toString()}; }`); } + + const dndBackgroundColor = theme.getColor(TERMINAL_DRAG_AND_DROP_BACKGROUND) || theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND); + console.log('dndBackgroundColor', dndBackgroundColor); + if (dndBackgroundColor) { + collector.addRule(`.monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay { background-color: ${dndBackgroundColor.toString()}; }`); + } }); diff --git a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts index 30c7c55d9e4..e9847286a0b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { registerColor, ColorIdentifier, ColorDefaults } from 'vs/platform/theme/common/colorRegistry'; -import { PANEL_BORDER } from 'vs/workbench/common/theme'; +import { EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_BORDER } from 'vs/workbench/common/theme'; /** * The color identifiers for the terminal's ansi colors. The index in the array corresponds to the index @@ -32,6 +32,11 @@ export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', { light: PANEL_BORDER, hc: PANEL_BORDER }, nls.localize('terminal.border', 'The color of the border that separates split panes within the terminal. This defaults to panel.border.')); +export const TERMINAL_DRAG_AND_DROP_BACKGROUND = registerColor('terminal.dropBackground', { + dark: EDITOR_DRAG_AND_DROP_BACKGROUND, + light: EDITOR_DRAG_AND_DROP_BACKGROUND, + hc: EDITOR_DRAG_AND_DROP_BACKGROUND +}, nls.localize('terminal.dragAndDropBackground', "Background color when dragging on top of terminals. The color should have transparency so that the terminal contents can still shine through.")); export const ansiColorMap: { [key: string]: { index: number, defaults: ColorDefaults } } = { 'terminal.ansiBlack': { From 0ec51535fa205d11f2f3bba06e8e9cb5cfd2f98a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 19 May 2021 13:03:44 -0700 Subject: [PATCH 157/270] Rearrange actual group splits, not just instances --- src/vs/workbench/contrib/terminal/browser/terminalGroup.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 27dbf932e67..98b09532c72 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -386,6 +386,10 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } this._terminalInstances.splice(sourceIndex, 1); this._terminalInstances.splice(index, 0, instance); + if (this._splitPaneContainer) { + this._splitPaneContainer.remove(instance); + this._splitPaneContainer.split(instance, sourceIndex < index ? index - 1 : index); + } this._onInstancesChanged.fire(); } From 8c27c1f257b44acdf2c143df907eb975be89db35 Mon Sep 17 00:00:00 2001 From: Miguel Solorio Date: Wed, 19 May 2021 13:13:29 -0700 Subject: [PATCH 158/270] Update list focus highlight color in standaloned editor Refs #123703 --- .../standalone/browser/quickInput/standaloneQuickInput.css | 2 +- src/vs/editor/standalone/common/themes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css index fc302f49d68..c48737cfaf4 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css @@ -14,7 +14,7 @@ .vs .quick-input-widget .monaco-list-row.focused .monaco-highlighted-label .highlight, .vs .quick-input-widget .monaco-list-row.focused .monaco-highlighted-label .highlight { - color: #33B6FF; + color: #9DDDFF; } .vs-dark .quick-input-widget .monaco-highlighted-label .highlight, diff --git a/src/vs/editor/standalone/common/themes.ts b/src/vs/editor/standalone/common/themes.ts index 24084dacb5e..cf0b7945091 100644 --- a/src/vs/editor/standalone/common/themes.ts +++ b/src/vs/editor/standalone/common/themes.ts @@ -74,7 +74,7 @@ export const vs: IStandaloneThemeData = { [editorIndentGuides]: '#D3D3D3', [editorActiveIndentGuides]: '#939393', [editorSelectionHighlight]: '#ADD6FF4D', - [listFocusHighlightForeground]: '#33B6FF' + [listFocusHighlightForeground]: '#9DDDFF' } }; /* -------------------------------- End vs theme -------------------------------- */ From 26194faa8be18e79603a65e99b25153a4c5ed9b9 Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 19 May 2021 13:35:39 -0700 Subject: [PATCH 159/270] fix focus indicator default value. --- src/vs/workbench/contrib/notebook/common/notebookOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index 951b6b16b34..c778ff425ba 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -143,7 +143,7 @@ export class NotebookOptions { } if (focusIndicator) { - configuration.focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator); + configuration.focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator) ?? 'border'; } if (compactView) { From 08f4a36de15c40ac3b6b4a65f1ca5f90ea3c2711 Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 19 May 2021 13:49:46 -0700 Subject: [PATCH 160/270] notebook toolbar container display none by default. --- src/vs/workbench/contrib/notebook/browser/media/notebook.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 8bf527a75ab..cc070968973 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -13,7 +13,7 @@ .monaco-workbench .notebookOverlay .notebook-toolbar-container { width: 100%; - display: flex; + display: none; margin-top: 2px; margin-bottom: 2px; } From c936add60132183432497fecc4c3fb819624e7be Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 19 May 2021 14:07:13 -0700 Subject: [PATCH 161/270] hide kernel status bar item if it is already rendered in notebook toolbar. --- .../browser/contrib/status/editorStatus.ts | 7 +++++++ .../notebook/browser/notebookEditorToolbar.ts | 7 ++++--- .../contrib/notebook/common/notebookCommon.ts | 1 + .../contrib/notebook/common/notebookOptions.ts | 16 +++++++++++++--- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index f06c862be27..f6334bcde3f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -242,6 +242,12 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { } const updateStatus = () => { + if (activeEditor.notebookOptions.getLayoutConfiguration().globalToolbar) { + // kernel info rendered in the notebook toolbar already + this._kernelInfoElement.clear(); + return; + } + const notebook = activeEditor.viewModel?.notebookDocument; if (notebook) { this._showKernelStatus(notebook); @@ -254,6 +260,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { this._editorDisposables.add(this._notebookKernelService.onDidChangeNotebookKernelBinding(updateStatus)); this._editorDisposables.add(this._notebookKernelService.onDidChangeNotebookAffinity(updateStatus)); this._editorDisposables.add(activeEditor.onDidChangeModel(updateStatus)); + this._editorDisposables.add(activeEditor.notebookOptions.onDidChangeOptions(updateStatus)); updateStatus(); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts index 3fc2773c8bb..0d45a0ab1dd 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts @@ -23,6 +23,7 @@ import { INotebookEditor, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebo import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem'; import { ActionViewWithLabel } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { ExperimentalGlobalToolbar } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; @@ -97,10 +98,10 @@ export class NotebookEditorToolbar extends Disposable { this._notebookGlobalActionsMenu = this._register(cellMenu.getNotebookToolbar(this.contextKeyService)); this._register(this._notebookGlobalActionsMenu); - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar') ?? false; + this._useGlobalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('notebook.experimental.globalToolbar')) { - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar'); + if (e.affectsConfiguration(ExperimentalGlobalToolbar)) { + this._useGlobalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar); this._showNotebookActionsinEditorToolbar(); } })); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index f3fd40adfee..d8fcf0068cb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -899,6 +899,7 @@ export const ExperimentalUseMarkdownRenderer = 'notebook.experimental.useMarkdow export const ExperimentalCompactView = 'notebook.experimental.compactView'; export const ExperimentalFocusIndicator = 'notebook.experimental.cellFocusIndicator'; export const ExperimentalInsertToolbarPosition = 'notebook.experimental.insertToolbarPosition'; +export const ExperimentalGlobalToolbar = 'notebook.experimental.globalToolbar'; export const ExperimentalUndoRedoPerCell = 'notebook.experimental.undoRedoPerCell'; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index c778ff425ba..2dde37f10d1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -6,7 +6,7 @@ import { Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalFocusIndicator, ExperimentalInsertToolbarPosition, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalFocusIndicator, ExperimentalGlobalToolbar, ExperimentalInsertToolbarPosition, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; const SCROLLABLE_ELEMENT_PADDING_TOP = 18; @@ -49,6 +49,7 @@ export interface NotebookLayoutConfiguration { compactView: boolean; focusIndicator: 'border' | 'gutter'; insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'; + globalToolbar: boolean; } interface NotebookOptionsChangeEvent { @@ -59,6 +60,7 @@ interface NotebookOptionsChangeEvent { compactView?: boolean; focusIndicator?: boolean; insertToolbarPosition?: boolean; + globalToolbar?: boolean; } const defaultConfigConstants = { @@ -85,6 +87,7 @@ export class NotebookOptions { constructor(readonly configurationService: IConfigurationService) { const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); + const globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); const cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); const compactView = this.configurationService.getValue(ExperimentalCompactView); @@ -109,6 +112,7 @@ export class NotebookOptions { editorBottomPaddingWithoutStatusBar: 12, collapsedIndicatorHeight: 24, showCellStatusBar, + globalToolbar, cellToolbarLocation, cellToolbarInteraction, compactView, @@ -123,8 +127,9 @@ export class NotebookOptions { let compactView = e.affectsConfiguration(ExperimentalCompactView); let focusIndicator = e.affectsConfiguration(ExperimentalFocusIndicator); let insertToolbarPosition = e.affectsConfiguration(ExperimentalInsertToolbarPosition); + let globalToolbar = e.affectsConfiguration(ExperimentalGlobalToolbar); - if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator && !insertToolbarPosition) { + if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator && !insertToolbarPosition && !globalToolbar) { return; } @@ -161,6 +166,10 @@ export class NotebookOptions { configuration.bottomToolbarGap = bottomToolbarGap; } + if (globalToolbar) { + configuration.globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; + } + this._layoutConfiguration = configuration; // trigger event @@ -170,7 +179,8 @@ export class NotebookOptions { cellToolbarInteraction: cellToolbarInteraction, compactView: compactView, focusIndicator: focusIndicator, - insertToolbarPosition: insertToolbarPosition + insertToolbarPosition: insertToolbarPosition, + globalToolbar: globalToolbar }); })); From 0d338068034d7c42b9539ba35e816cff3ecf92b2 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 May 2021 10:51:19 -0700 Subject: [PATCH 162/270] Add suiteRepeat test util --- extensions/vscode-api-tests/src/utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index 83dc7aa4558..b346efefce9 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -143,3 +143,9 @@ export function testRepeat(n: number, description: string, callback: (this: any) test(`${description} (iteration ${i})`, callback); } } + +export function suiteRepeat(n: number, description: string, callback: (this: any) => any): void { + for (let i = 0; i < n; i++) { + suite(`${description} (iteration ${i})`, callback); + } +} From c1d6e44262e6b7836c12d14d2db80f4dc95514ad Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 May 2021 12:47:31 -0700 Subject: [PATCH 163/270] Avoid listener leak warning due to reusing cancel token for many RPC calls --- .../contributedStatusBarItemController.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts index 7d855e950d0..5f9e5bbd99d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts @@ -6,7 +6,7 @@ import { flatten } from 'vs/base/common/arrays'; import { Throttler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICellVisibilityChangeEvent, NotebookVisibleCellObserver } from 'vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver'; import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; @@ -69,7 +69,7 @@ class CellStatusBarHelper extends Disposable { private _currentItemIds: string[] = []; private _currentItemLists: INotebookCellStatusBarItemList[] = []; - private readonly _cancelTokenSource: CancellationTokenSource; + private _activeToken = new MutableDisposable(); private readonly _updateThrottler = new Throttler(); @@ -80,9 +80,6 @@ class CellStatusBarHelper extends Disposable { ) { super(); - this._cancelTokenSource = new CancellationTokenSource(); - this._register(toDisposable(() => this._cancelTokenSource.dispose(true))); - this._updateSoon(); this._register(this._cell.model.onDidChangeContent(() => this._updateSoon())); this._register(this._cell.model.onDidChangeLanguage(() => this._updateSoon())); @@ -102,12 +99,16 @@ class CellStatusBarHelper extends Disposable { const cellIndex = this._notebookViewModel.getCellIndex(this._cell); const docUri = this._notebookViewModel.notebookDocument.uri; const viewType = this._notebookViewModel.notebookDocument.viewType; - const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, this._cancelTokenSource.token); - if (this._cancelTokenSource.token.isCancellationRequested) { + this._activeToken.value = new CancellationTokenSource(); + const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, this._activeToken.value.token); + if (this._activeToken.value.token.isCancellationRequested) { itemLists.forEach(itemList => itemList.dispose && itemList.dispose()); + this._activeToken.clear(); return; } + this._activeToken.clear(); + const items = flatten(itemLists.map(itemList => itemList.items)); const newIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); From 1642d4cd0add16a9f5128d178c5f3163bc589bdd Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 May 2021 14:29:22 -0700 Subject: [PATCH 164/270] Merge groupBy and groupByNumber utils Fix #123569 --- src/vs/base/common/collections.ts | 22 +--------- src/vs/base/test/common/collections.test.ts | 22 ---------- .../browser/viewModel/notebookViewModel.ts | 44 ++++++++++--------- 3 files changed, 25 insertions(+), 63 deletions(-) diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index cc037ecfd71..24ed68bae5f 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -53,8 +53,8 @@ export function forEach(from: IStringDictionary | INumberDictionary, ca * Groups the collection into a dictionary based on the provided * group function. */ -export function groupBy(data: T[], groupFn: (element: T) => string): IStringDictionary { - const result: IStringDictionary = Object.create(null); +export function groupBy(data: V[], groupFn: (element: V) => K): Record { + const result: Record = Object.create(null); for (const element of data) { const key = groupFn(element); let target = result[key]; @@ -66,24 +66,6 @@ export function groupBy(data: T[], groupFn: (element: T) => string): IStringD return result; } -/** - * Groups the collection into a dictionary based on the provided - * group function. - */ -export function groupByNumber(data: T[], groupFn: (element: T) => number): Map { - const result = new Map(); - for (const element of data) { - const key = groupFn(element); - let target = result.get(key); - if (!target) { - target = []; - result.set(key, target); - } - target.push(element); - } - return result; -} - export function fromMap(original: Map): IStringDictionary { const result: IStringDictionary = Object.create(null); if (original) { diff --git a/src/vs/base/test/common/collections.test.ts b/src/vs/base/test/common/collections.test.ts index 881525a006b..a86eeab2de0 100644 --- a/src/vs/base/test/common/collections.test.ts +++ b/src/vs/base/test/common/collections.test.ts @@ -53,26 +53,4 @@ suite('Collections', () => { assert.strictEqual(grouped[group2].length, 1); assert.strictEqual(grouped[group2][0].value, value3); }); - - test('groupByNumber', () => { - - const group1 = 1, group2 = 2; - const value1 = 'a', value2 = 'b', value3 = 'c'; - let source = [ - { key: group1, value: value1 }, - { key: group1, value: value2 }, - { key: group2, value: value3 }, - ]; - - let grouped = collections.groupByNumber(source, x => x.key); - - // Group 1 - assert.strictEqual(grouped.get(group1)!.length, 2); - assert.strictEqual(grouped.get(group1)![0].value, value1); - assert.strictEqual(grouped.get(group1)![1].value, value2); - - // Group 2 - assert.strictEqual(grouped.get(group2)!.length, 1); - assert.strictEqual(grouped.get(group2)![0].value, value3); - }); }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 840b8621878..4e8b53f69a1 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -3,40 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { groupBy } from 'vs/base/common/collections'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; +import { dirname } from 'vs/base/common/resources'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, IReadonlyTextBuffer, EndOfLinePreference } from 'vs/editor/common/model'; +import { EndOfLinePreference, IModelDecorationOptions, IModelDeltaDecoration, IReadonlyTextBuffer, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/editor/common/model/editStack'; import { IntervalNode, IntervalTree } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellEditState, CellFindMatch, ICellViewModel, NotebookLayoutInfo, INotebookDeltaDecoration, INotebookDeltaCellStatusBarItems, CellFocusMode, CellFindMatchWithIndex } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; +import { CellEditState, CellFindMatch, CellFindMatchWithIndex, CellFocusMode, ICellViewModel, INotebookDeltaCellStatusBarItems, INotebookDeltaDecoration, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookSearchOptions, NotebookCellsChangeType, ICell, NotebookCellTextModelSplice, CellEditType, IOutputDto, SelectionStateType, ISelectionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ICellRange, cellIndexesToRanges, cellRangesToIndexes, reduceRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; -import { dirname } from 'vs/base/common/resources'; -import { IPosition, Position } from 'vs/editor/common/core/position'; -import { MultiModelEditStackElement, SingleModelEditStackElement } from 'vs/editor/common/model/editStack'; -import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; -import { NotebookCellSelectionCollection } from 'vs/workbench/contrib/notebook/browser/viewModel/cellSelectionCollection'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { groupByNumber } from 'vs/base/common/collections'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellEditType, CellKind, ICell, INotebookSearchOptions, IOutputDto, ISelectionState, NotebookCellMetadata, NotebookCellsChangeType, NotebookCellTextModelSplice, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { cellIndexesToRanges, cellRangesToIndexes, ICellRange, reduceRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; export interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; @@ -730,13 +730,13 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } deltaCellStatusBarItems(oldItems: string[], newItems: INotebookDeltaCellStatusBarItems[]): string[] { - const deletesByHandle = groupByNumber(oldItems, id => this._statusBarItemIdToCellMap.get(id) ?? -1); + const deletesByHandle = groupBy(oldItems, id => this._statusBarItemIdToCellMap.get(id) ?? -1); const result: string[] = []; newItems.forEach(itemDelta => { const cell = this.getCellByHandle(itemDelta.handle); - const deleted = deletesByHandle.get(itemDelta.handle) ?? []; - deletesByHandle.delete(itemDelta.handle); + const deleted = deletesByHandle[itemDelta.handle] ?? []; + delete deletesByHandle[itemDelta.handle]; const ret = cell?.deltaCellStatusBarItems(deleted, itemDelta.items) || []; ret.forEach(id => { this._statusBarItemIdToCellMap.set(id, itemDelta.handle); @@ -745,10 +745,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD result.push(...ret); }); - deletesByHandle.forEach((ids, handle) => { + for (let _handle in deletesByHandle) { + const handle = parseInt(_handle); + const ids = deletesByHandle[handle]; const cell = this.getCellByHandle(handle); cell?.deltaCellStatusBarItems(ids, []); - }); + } return result; } From d921cc41fc030271c699eb0d5b1c2cdfec304eda Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 15:34:07 -0700 Subject: [PATCH 165/270] Update markdown grammar --- extensions/markdown-basics/cgmanifest.json | 2 +- .../syntaxes/markdown.tmLanguage.json | 106 +++++++++--------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 0d320ab3376..f3f0717c5ad 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "399ff6f608a7bef3f68713be23cdcb4c6d475804" + "commitHash": "a612b96d62aa1ce305c4a55dc9d577316fab39da" } }, "license": "MIT", diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index 66f8114869a..aaa4c774b40 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/399ff6f608a7bef3f68713be23cdcb4c6d475804", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/a612b96d62aa1ce305c4a55dc9d577316fab39da", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -63,7 +63,7 @@ "while": "(^|\\G)\\s*(>) ?" }, "fenced_code_block_css": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -96,7 +96,7 @@ ] }, "fenced_code_block_basic": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -129,7 +129,7 @@ ] }, "fenced_code_block_ini": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -162,7 +162,7 @@ ] }, "fenced_code_block_java": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -195,7 +195,7 @@ ] }, "fenced_code_block_lua": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -228,7 +228,7 @@ ] }, "fenced_code_block_makefile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -261,7 +261,7 @@ ] }, "fenced_code_block_perl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -294,7 +294,7 @@ ] }, "fenced_code_block_r": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -327,7 +327,7 @@ ] }, "fenced_code_block_ruby": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -360,7 +360,7 @@ ] }, "fenced_code_block_php": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -396,7 +396,7 @@ ] }, "fenced_code_block_sql": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -429,7 +429,7 @@ ] }, "fenced_code_block_vs_net": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -462,7 +462,7 @@ ] }, "fenced_code_block_xml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -495,7 +495,7 @@ ] }, "fenced_code_block_xsl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -528,7 +528,7 @@ ] }, "fenced_code_block_yaml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -561,7 +561,7 @@ ] }, "fenced_code_block_dosbatch": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -594,7 +594,7 @@ ] }, "fenced_code_block_clojure": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -627,7 +627,7 @@ ] }, "fenced_code_block_coffee": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -660,7 +660,7 @@ ] }, "fenced_code_block_c": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -693,7 +693,7 @@ ] }, "fenced_code_block_cpp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -726,7 +726,7 @@ ] }, "fenced_code_block_diff": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -759,7 +759,7 @@ ] }, "fenced_code_block_dockerfile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -792,7 +792,7 @@ ] }, "fenced_code_block_git_commit": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -825,7 +825,7 @@ ] }, "fenced_code_block_git_rebase": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -858,7 +858,7 @@ ] }, "fenced_code_block_go": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -891,7 +891,7 @@ ] }, "fenced_code_block_groovy": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -924,7 +924,7 @@ ] }, "fenced_code_block_pug": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -957,7 +957,7 @@ ] }, "fenced_code_block_js": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -990,7 +990,7 @@ ] }, "fenced_code_block_js_regexp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1023,7 +1023,7 @@ ] }, "fenced_code_block_json": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1056,7 +1056,7 @@ ] }, "fenced_code_block_jsonc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1089,7 +1089,7 @@ ] }, "fenced_code_block_less": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1122,7 +1122,7 @@ ] }, "fenced_code_block_objc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1155,7 +1155,7 @@ ] }, "fenced_code_block_swift": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1188,7 +1188,7 @@ ] }, "fenced_code_block_scss": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1221,7 +1221,7 @@ ] }, "fenced_code_block_perl6": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1254,7 +1254,7 @@ ] }, "fenced_code_block_powershell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1287,7 +1287,7 @@ ] }, "fenced_code_block_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1320,7 +1320,7 @@ ] }, "fenced_code_block_regexp_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1353,7 +1353,7 @@ ] }, "fenced_code_block_rust": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1386,7 +1386,7 @@ ] }, "fenced_code_block_scala": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1419,7 +1419,7 @@ ] }, "fenced_code_block_shell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1452,7 +1452,7 @@ ] }, "fenced_code_block_ts": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1485,7 +1485,7 @@ ] }, "fenced_code_block_tsx": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1518,7 +1518,7 @@ ] }, "fenced_code_block_csharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1551,7 +1551,7 @@ ] }, "fenced_code_block_fsharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1584,7 +1584,7 @@ ] }, "fenced_code_block_dart": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1617,7 +1617,7 @@ ] }, "fenced_code_block_handlebars": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1650,7 +1650,7 @@ ] }, "fenced_code_block_markdown": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1683,7 +1683,7 @@ ] }, "fenced_code_block_log": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1716,7 +1716,7 @@ ] }, "fenced_code_block_erlang": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1749,7 +1749,7 @@ ] }, "fenced_code_block_elixir": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { From f433b4781a0c719a17b1c23e67d1ed0161afd972 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 15:47:35 -0700 Subject: [PATCH 166/270] Pick up TS 4.3.1-rc --- extensions/package.json | 2 +- extensions/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/package.json b/extensions/package.json index f590e5cab37..9b5fd61b741 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^4.3.0-dev.20210507" + "typescript": "^4.3.1-rc" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 18e5f36add7..3d37fa10930 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -24,10 +24,10 @@ fast-plist@0.1.2: resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -typescript@^4.3.0-dev.20210507: - version "4.3.0-dev.20210507" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.0-dev.20210507.tgz#07fdc0479bb1b215865aabb01ed1d920cf844ea0" - integrity sha512-SEZV+XOg8exwPXlTmxPT94v9kasblelh4TjL1I12FBv0DiorBHDtUs8GC2h2sg8zJOgFwj06QXiaLLGL5RhzDw== +typescript@^4.3.1-rc: + version "4.3.1-rc" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.1-rc.tgz#925149c8d8514e20a6bd8d4bd7f42adac67ab59c" + integrity sha512-L3uJ0gcntaRaKni9aV2amYB+pCDVodKe/B5+IREyvtKGsDOF7cYjchHb/B894skqkgD52ykRuWatIZMqEsHIqA== vscode-grammar-updater@^1.0.3: version "1.0.3" From 9ee1906718de45930996f2f229e2bff28c6b9dc6 Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Wed, 19 May 2021 16:57:12 -0600 Subject: [PATCH 167/270] pr feedback and combining prevEditor info for preview and non-preview --- .../src/features/preview.ts | 5 +- .../src/features/previewManager.ts | 14 ++++-- .../src/util/topmostLineMonitor.ts | 49 +++++-------------- 3 files changed, 23 insertions(+), 45 deletions(-) diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 0254ed42e40..83ef8d18106 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -533,7 +533,7 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown })); this._register(this.preview.onScroll((scrollInfo) => { - topmostLineMonitor.setPreviousStaticEditorLine(scrollInfo); + topmostLineMonitor.setPreviousEditorLine(scrollInfo); })); this._register(topmostLineMonitor.onDidChanged(event => { @@ -541,9 +541,6 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown this.preview.scrollTo(event.line); } })); - - const currentLine = this.preview.state.line ? this.preview.state.line : 0; - topmostLineMonitor.setPreviousStaticEditorLine({ line: currentLine, uri: this.preview.resource }); } private readonly _onDispose = this._register(new vscode.EventEmitter()); diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 738286d2091..7f30abb1c84 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -12,6 +12,7 @@ import { TopmostLineMonitor } from '../util/topmostLineMonitor'; import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview, scrollEditorToLine } from './preview'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider } from './previewContentProvider'; +import { isMarkdownFile } from '../util/file'; export interface DynamicPreviewSettings { readonly resourceColumn: vscode.ViewColumn; @@ -76,8 +77,15 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this)); this._register(vscode.window.registerCustomEditorProvider(this.customEditorViewType, this)); - this._register(this._topmostLineMonitor.onEditorNeedsScrolling(event => { - scrollEditorToLine(event.line, event.editor); + this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => { + + // When at a markdown file, apply existing scroll settings + if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) { + const line = this._topmostLineMonitor.getPreviousEditorLineByUri(textEditor.document.uri); + if (line) { + scrollEditorToLine(line, textEditor); + } + } })); } @@ -164,7 +172,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview document: vscode.TextDocument, webview: vscode.WebviewPanel ): Promise { - const lineNumber = this._topmostLineMonitor.getPreviousMDTextEditorLineByUri(document.uri); + const lineNumber = this._topmostLineMonitor.getPreviousEditorLineByUri(document.uri); const preview = StaticMarkdownPreview.revive( document.uri, webview, diff --git a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index ad89388c3c7..1d4d1e3e738 100644 --- a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -16,15 +16,16 @@ export class TopmostLineMonitor extends Disposable { private readonly pendingUpdates = new Map(); private readonly throttle = 50; - private previousMDTextEditors = new Map(); - private previousStaticEditorInfo = new Map(); - private isPrevEditorCustom = false; + private previousEditorInfo = new Map(); + public isPrevEditorCustom = false; constructor() { super(); if (vscode.window.activeTextEditor) { - this.setPreviousMDTextEditorLine(vscode.window.activeTextEditor); + + const line = getVisibleLine(vscode.window.activeTextEditor); + this.setPreviousEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 }); } this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => { @@ -32,54 +33,26 @@ export class TopmostLineMonitor extends Disposable { const line = getVisibleLine(event.textEditor); if (typeof line === 'number') { this.updateLine(event.textEditor.document.uri, line); + this.setPreviousEditorLine({ uri: event.textEditor.document.uri, line: line }); } } })); - this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => { - - // When at a markdown file, apply existing scroll settings from static preview if last editor was custom. - // Also save reference to text editor for line number reference later - if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) { - if (this.isPrevEditorCustom) { - const line = this.getPreviousStaticEditorLineByUri(textEditor.document.uri); - if (line) { - this._onEditorNeedsScrolling.fire({ line: line, editor: textEditor }); - } - } - this.setPreviousMDTextEditorLine(textEditor); - } - - this.isPrevEditorCustom = (textEditor === undefined); - })); } private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); public readonly onDidChanged = this._onChanged.event; - private readonly _onEditorNeedsScrolling = this._register(new vscode.EventEmitter<{ readonly line: number, readonly editor: vscode.TextEditor }>()); - public readonly onEditorNeedsScrolling = this._onEditorNeedsScrolling.event; - - public setPreviousMDTextEditorLine(editor: vscode.TextEditor) { - const uri = editor.document.uri; - this.previousMDTextEditors.set(uri.toString(), editor); + public setPreviousEditorLine(scrollLocation: LastScrollLocation): void { + this.previousEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); } - public getPreviousMDTextEditorLineByUri(resource: vscode.Uri) { - const editor = this.previousMDTextEditors.get(resource.toString()); - return editor?.visibleRanges[0].start.line; - } - - public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation) { - this.previousStaticEditorInfo.set(scrollLocation.uri.toString(), scrollLocation); - } - - public getPreviousStaticEditorLineByUri(resource: vscode.Uri) { - const scrollLoc = this.previousStaticEditorInfo.get(resource.toString()); + public getPreviousEditorLineByUri(resource: vscode.Uri): number | undefined { + const scrollLoc = this.previousEditorInfo.get(resource.toString()); return scrollLoc?.line; } - private updateLine( + public updateLine( resource: vscode.Uri, line: number ) { From 920c9a3a0d5d8bc3d6a4f63535052aea2aedccf7 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 19 May 2021 15:57:52 -0700 Subject: [PATCH 168/270] improve banner for virtual workspace --- .../browser/parts/banner/bannerPart.ts | 12 +- .../browser/workspace.contribution.ts | 108 +++++++++++++++--- .../services/banner/browser/bannerService.ts | 3 +- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index a894eafc722..0a9e336bb29 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -10,7 +10,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Codicon, registerCodicon } from 'vs/base/common/codicons'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { Part } from 'vs/workbench/browser/part'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; @@ -96,9 +96,9 @@ export class BannerPart extends Part implements IBannerService { constructor( @IThemeService themeService: IThemeService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @IStorageService storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IStorageService private readonly storageService: IStorageService, ) { super(Parts.BANNER_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -131,8 +131,8 @@ export class BannerPart extends Part implements IBannerService { clearNode(this.element); // Remember choice - if (item.scope) { - this.storageService.store(item.id, true, item.scope, StorageTarget.USER); + if (typeof item.onClose === 'function') { + item.onClose(); } this.item = undefined; @@ -211,10 +211,6 @@ export class BannerPart extends Part implements IBannerService { } show(item: IBannerItem): void { - if (item.scope && this.storageService.getBoolean(item.id, item.scope, false)) { - return; - } - if (item.id === this.item?.id) { this.setVisibility(true); return; diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 4abb2b498c6..0dbac3b6736 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -16,7 +16,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, workspaceTrustToString } from 'vs/platform/workspace/common/workspaceTrust'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Codicon } from 'vs/base/common/codicons'; +import { Codicon, registerCodicon } from 'vs/base/common/codicons'; import { ThemeColor } from 'vs/workbench/api/common/extHostTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -47,7 +47,13 @@ import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts import { verifyMicrosoftInternalDomain } from 'vs/platform/telemetry/common/commonProperties'; const BANNER_RESTRICTED_MODE = 'workbench.banner.restrictedMode'; +const BANNER_VIRTUAL_WORKSPACE = 'workbench.banner.virtualWorkspace'; +const BANNER_VIRTUAL_AND_RESTRICTED = 'workbench.banner.virtualAndRestricted'; const STARTUP_PROMPT_SHOWN_KEY = 'workspace.trust.startupPrompt.shown'; +const BANNER_RESTRICTED_MODE_DISMISSED_KEY = 'workbench.banner.restrictedMode.dismissed'; +const BANNER_VIRTUAL_WORKSPACE_DISMISSED_KEY = 'workbench.banner.virtualWorkspace.dismissed'; + +const infoIcon = registerCodicon('workspace-banner-warning-icon', Codicon.info); /* * Trust Request UX Handler @@ -149,6 +155,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben private showModalOnStart(): void { if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { + this.updateWorkbenchIndicators(true); return; } @@ -198,13 +205,52 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben this.statusbarService.updateEntryVisibility(this.entryId, false); } - private getBannerItem(): IBannerItem { - return { - id: BANNER_RESTRICTED_MODE, - icon: shieldIcon, - ariaLabel: localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use navigation keys to access banner actions."), - message: localize('restrictedModeBannerMessage', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."), - actions: [ + private getBannerItem(isVirtualWorkspace: boolean, restrictedMode: boolean): IBannerItem | undefined { + + const dismissedVirtual = this.storageService.getBoolean(BANNER_VIRTUAL_WORKSPACE_DISMISSED_KEY, StorageScope.WORKSPACE, false); + const dismissedRestricted = this.storageService.getBoolean(BANNER_RESTRICTED_MODE_DISMISSED_KEY, StorageScope.WORKSPACE, false); + + // all important info has been dismissed + if (dismissedVirtual && dismissedRestricted) { + return undefined; + } + + // don't show restricted mode only banner + if (dismissedRestricted && !isVirtualWorkspace) { + return undefined; + } + + // don't show virtual workspace only banner + if (dismissedVirtual && !restrictedMode) { + return undefined; + } + + const choose = (virtual: any, restricted: any, virtualAndRestricted: any) => { + return (isVirtualWorkspace && !dismissedVirtual) && (restrictedMode && !dismissedRestricted) ? virtualAndRestricted : ((isVirtualWorkspace && !dismissedVirtual) ? virtual : restricted); + }; + + const id = choose(BANNER_VIRTUAL_WORKSPACE, BANNER_RESTRICTED_MODE, BANNER_VIRTUAL_AND_RESTRICTED); + const icon = choose(infoIcon, shieldIcon, infoIcon); + const ariaLabel = choose( + localize('virtualBannerAriaLabel', "Some features are not available because the current workspace is backed by a virtual file system. Use navigation keys to access banner actions."), + localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use navigation keys to access banner actions."), + localize('virtualAndRestrictedModeBannerAriaLabel', "Some features are not available because the current workspace is backed by a virtual file system and is not trusted. You can trust this workspace to enable some of these features. Use navigation keys to access banner actions."), + ); + + const message = choose( + localize('virtualBannerMessage', "Some features are not available because the current workspace is backed by a virtual file system."), + localize('restrictedModeBannerMessage', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."), + localize('virtualAndRestrictedModeBannerMessage', "Some features are not available because the current workspace is backed by a virtual file system and is not trusted. You can trust this workspace to enabled some of these features."), + ); + + const actions = choose( + [ + { + label: localize('virtualBannerLearnMore', "Learn More"), + href: 'https://aka.ms/vscode-virtual-workspaces' + } + ], + [ { label: localize('restrictedModeBannerManage', "Manage"), href: 'command:workbench.trust.manage' @@ -214,7 +260,33 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben href: 'https://aka.ms/vscode-workspace-trust' } ], - scope: StorageScope.WORKSPACE, + [ + { + label: localize('virtualAndRestrictedModeBannerManage', "Manage Trust"), + href: 'command:workbench.trust.manage' + }, + { + label: localize('virtualBannerLearnMore', "Learn More"), + href: 'https://aka.ms/vscode-virtual-workspaces' + } + ] + ); + + return { + id, + icon, + ariaLabel, + message, + actions, + onClose: () => { + if (isVirtualWorkspace) { + this.storageService.store(BANNER_VIRTUAL_WORKSPACE_DISMISSED_KEY, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + if (restrictedMode) { + this.storageService.store(BANNER_RESTRICTED_MODE_DISMISSED_KEY, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + } }; } @@ -245,10 +317,20 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben private updateWorkbenchIndicators(trusted: boolean): void { this.updateStatusbarEntry(trusted); - if (!trusted) { - this.bannerService.show(this.getBannerItem()); - } else { - this.bannerService.hide(BANNER_RESTRICTED_MODE); + + const isVirtualWorkspace = getVirtualWorkspaceScheme(this.workspaceContextService.getWorkspace()) !== undefined; + const bannerItem = this.getBannerItem(isVirtualWorkspace, !trusted); + + if (bannerItem) { + if (!isVirtualWorkspace) { + if (!trusted) { + this.bannerService.show(bannerItem); + } else { + this.bannerService.hide(BANNER_RESTRICTED_MODE); + } + } else { + this.bannerService.show(bannerItem); + } } } diff --git a/src/vs/workbench/services/banner/browser/bannerService.ts b/src/vs/workbench/services/banner/browser/bannerService.ts index 859bf6035e8..15f9fe15afd 100644 --- a/src/vs/workbench/services/banner/browser/bannerService.ts +++ b/src/vs/workbench/services/banner/browser/bannerService.ts @@ -7,16 +7,15 @@ import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILinkDescriptor } from 'vs/platform/opener/browser/link'; -import { StorageScope } from 'vs/platform/storage/common/storage'; export interface IBannerItem { readonly id: string; readonly icon: Codicon; readonly message: string | MarkdownString; - readonly scope?: StorageScope; /* Used to remember that the banner has been closed. */ readonly actions?: ILinkDescriptor[]; readonly ariaLabel?: string; + readonly onClose?: () => void; } export const IBannerService = createDecorator('bannerService'); From 41606da2dc0d8817db7d79ddd81eedc46508620c Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Wed, 19 May 2021 16:59:26 -0600 Subject: [PATCH 169/270] removing unecessary spaces --- extensions/markdown-language-features/src/features/preview.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 83ef8d18106..23ce0b495b0 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -286,8 +286,6 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { }); } - - private async updatePreview(forceUpdate?: boolean): Promise { clearTimeout(this.throttleTimer); this.throttleTimer = undefined; From f594bb47e590443b52faeea42f5b739fe521d2e0 Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Wed, 19 May 2021 17:00:55 -0600 Subject: [PATCH 170/270] removing more unecessary whitespace --- .../markdown-language-features/src/util/topmostLineMonitor.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts index 1d4d1e3e738..62f5b5c194b 100644 --- a/extensions/markdown-language-features/src/util/topmostLineMonitor.ts +++ b/extensions/markdown-language-features/src/util/topmostLineMonitor.ts @@ -23,7 +23,6 @@ export class TopmostLineMonitor extends Disposable { super(); if (vscode.window.activeTextEditor) { - const line = getVisibleLine(vscode.window.activeTextEditor); this.setPreviousEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 }); } @@ -37,7 +36,6 @@ export class TopmostLineMonitor extends Disposable { } } })); - } private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>()); From f822083cb70dd6636727632f59d04c34699f87cf Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 15:59:38 -0700 Subject: [PATCH 171/270] Check `defaultPrevented` before showing built-in webview context menu Fixes #123301 --- .../contrib/webview/browser/pre/main.js | 119 ++++++------------ 1 file changed, 36 insertions(+), 83 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 098abfbe8a1..7e429b1969b 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -4,12 +4,10 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check -/// - /** * @typedef {{ * postMessage: (channel: string, data?: any) => void, - * onMessage: (channel: string, handler: (event: MessageEvent, data: any) => void) => void, + * onMessage: (channel: string, handler: any) => void, * focusIframeOnCreate?: boolean, * ready?: Promise, * onIframeLoaded?: (iframe: HTMLIFrameElement) => void, @@ -58,18 +56,6 @@ const getPendingFrame = () => { return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame')); }; -/** - * @template T - * @param {T | undefined | null} obj - * @return {T} - */ -function assertIsDefined(obj) { - if (typeof obj === 'undefined' || obj === null) { - throw new Error('Found unexpected null'); - } - return obj; -} - const vscodePostMessageFuncName = '__vscode_post_message__'; const defaultStyles = document.createElement('style'); @@ -216,9 +202,6 @@ const workerReady = new Promise(async (resolve, reject) => { async registration => { await navigator.serviceWorker.ready; - /** - * @param {MessageEvent} event - */ const versionHandler = (event) => { if (event.data.channel !== 'version') { return; @@ -235,7 +218,7 @@ const workerReady = new Promise(async (resolve, reject) => { } }; navigator.serviceWorker.addEventListener('message', versionHandler); - assertIsDefined(registration.active).postMessage({ channel: 'version' }); + registration.active.postMessage({ channel: 'version' }); }, error => { reject(new Error(`Could not register service workers: ${error}.`)); @@ -248,7 +231,6 @@ const workerReady = new Promise(async (resolve, reject) => { export async function createWebviewManager(host) { // state let firstLoad = true; - /** @type {any} */ let loadTimeout; let styleVersion = 0; @@ -259,7 +241,7 @@ export async function createWebviewManager(host) { /** @type {number | undefined} */ initialScrollProgress: undefined, - /** @type {{ [key: string]: string } | undefined} */ + /** @type {{ [key: string]: string }} */ styles: undefined, /** @type {string | undefined} */ @@ -271,13 +253,13 @@ export async function createWebviewManager(host) { host.onMessage('did-load-resource', (_event, data) => { navigator.serviceWorker.ready.then(registration => { - assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); + registration.active.postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); }); }); host.onMessage('did-load-localhost', (_event, data) => { navigator.serviceWorker.ready.then(registration => { - assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data }); + registration.active.postMessage({ channel: 'did-load-localhost', data }); }); }); @@ -300,9 +282,7 @@ export async function createWebviewManager(host) { if (body) { body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast'); - if (initData.activeTheme) { - body.classList.add(initData.activeTheme); - } + body.classList.add(initData.activeTheme); body.dataset.vscodeThemeKind = initData.activeTheme; body.dataset.vscodeThemeName = initData.themeName || ''; @@ -455,9 +435,6 @@ export async function createWebviewManager(host) { let isHandlingScroll = false; - /** - * @param {WheelEvent} event - */ const handleWheel = (event) => { if (isHandlingScroll) { return; @@ -473,21 +450,15 @@ export async function createWebviewManager(host) { }); }; - /** - * @param {Event} event - */ const handleInnerScroll = (event) => { + if (!event.target || !event.target.body) { + return; + } if (isHandlingScroll) { return; } - const target = /** @type {HTMLDocument | null} */ (event.target); - const currentTarget = /** @type {Window | null} */ (event.currentTarget); - if (!target || !currentTarget || !target.body) { - return; - } - - const progress = currentTarget.scrollY / target.body.clientHeight; + const progress = event.currentTarget.scrollY / event.target.body.clientHeight; if (isNaN(progress)) { return; } @@ -504,19 +475,6 @@ export async function createWebviewManager(host) { }; /** - * @typedef {{ - * contents: string; - * options: { - * readonly allowScripts: boolean; - * readonly allowMultipleAPIAcquire: boolean; - * } - * state: any; - * resourceEndpoint: string; - * }} ContentUpdateData - */ - - /** - * @param {ContentUpdateData} data * @return {string} */ function toContentHtml(data) { @@ -526,10 +484,7 @@ export async function createWebviewManager(host) { newDocument.querySelectorAll('a').forEach(a => { if (!a.title) { - const href = a.getAttribute('href'); - if (typeof href === 'string') { - a.title = href; - } + a.title = a.getAttribute('href'); } }); @@ -554,11 +509,8 @@ export async function createWebviewManager(host) { try { // Attempt to rewrite CSPs that hardcode old-style resource endpoint const endpointUrl = new URL(data.resourceEndpoint); - const cspContent = csp.getAttribute('content'); - if (cspContent) { - const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); - csp.setAttribute('content', newCsp); - } + const newCsp = csp.getAttribute('content').replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); + csp.setAttribute('content', newCsp); } catch (e) { console.error(`Could not rewrite csp: ${e}`); } @@ -611,7 +563,7 @@ export async function createWebviewManager(host) { // update iframe-contents let updateId = 0; - host.onMessage('content', async (_event, /** @type {ContentUpdateData} */ data) => { + host.onMessage('content', async (_event, data) => { const currentUpdateId = ++updateId; try { @@ -634,19 +586,18 @@ export async function createWebviewManager(host) { const frame = getActiveFrame(); const wasFirstLoad = firstLoad; // keep current scrollY around and use later - /** @type {(body: HTMLElement, window: Window) => void} */ let setInitialScrollPosition; if (firstLoad) { firstLoad = false; setInitialScrollPosition = (body, window) => { - if (typeof initData.initialScrollProgress === 'number' && !isNaN(initData.initialScrollProgress)) { + if (!isNaN(initData.initialScrollProgress)) { if (window.scrollY === 0) { window.scroll(0, body.clientHeight * initData.initialScrollProgress); } } }; } else { - const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? assertIsDefined(frame.contentWindow).scrollY : 0; + const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentWindow.scrollY : 0; setInitialScrollPosition = (body, window) => { if (window.scrollY === 0) { window.scroll(0, scrollY); @@ -705,16 +656,15 @@ export async function createWebviewManager(host) { return; } - const contentDocument = assertIsDefined(newFrame.contentDocument); - if (contentDocument.readyState !== 'loading') { + if (newFrame.contentDocument.readyState !== 'loading') { clearInterval(interval); - onFrameLoaded(contentDocument); + onFrameLoaded(newFrame.contentDocument); } }, 10); } else { - assertIsDefined(newFrame.contentWindow).addEventListener('DOMContentLoaded', e => { + newFrame.contentWindow.addEventListener('DOMContentLoaded', e => { const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; - onFrameLoaded(assertIsDefined(contentDocument)); + onFrameLoaded(contentDocument); }); } @@ -742,7 +692,7 @@ export async function createWebviewManager(host) { newFrame.setAttribute('id', 'active-frame'); newFrame.style.visibility = 'visible'; if (host.focusIframeOnCreate) { - assertIsDefined(newFrame.contentWindow).focus(); + newFrame.contentWindow.focus(); } contentWindow.addEventListener('scroll', handleInnerScroll); @@ -770,12 +720,10 @@ export async function createWebviewManager(host) { loadTimeout = setTimeout(() => { clearTimeout(loadTimeout); loadTimeout = undefined; - onLoad(assertIsDefined(newFrame.contentDocument), assertIsDefined(newFrame.contentWindow)); + onLoad(newFrame.contentDocument, newFrame.contentWindow); }, 200); - const contentWindow = assertIsDefined(newFrame.contentWindow); - - contentWindow.addEventListener('load', function (e) { + newFrame.contentWindow.addEventListener('load', function (e) { const contentDocument = /** @type {Document} */ (e.target); if (loadTimeout) { @@ -786,11 +734,11 @@ export async function createWebviewManager(host) { }); // Bubble out various events - contentWindow.addEventListener('click', handleInnerClick); - contentWindow.addEventListener('auxclick', handleAuxClick); - contentWindow.addEventListener('keydown', handleInnerKeydown); - contentWindow.addEventListener('keyup', handleInnerUp); - contentWindow.addEventListener('contextmenu', e => { + newFrame.contentWindow.addEventListener('click', handleInnerClick); + newFrame.contentWindow.addEventListener('auxclick', handleAuxClick); + newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); + newFrame.contentWindow.addEventListener('keyup', handleInnerUp); + newFrame.contentWindow.addEventListener('contextmenu', e => { e.preventDefault(); host.postMessage('did-context-menu', { clientX: e.clientX, @@ -812,7 +760,7 @@ export async function createWebviewManager(host) { if (!pending) { const target = getActiveFrame(); if (target) { - assertIsDefined(target.contentWindow).postMessage(data.message, '*', data.transfer); + target.contentWindow.postMessage(data.message, '*', data.transfer); return; } } @@ -828,7 +776,7 @@ export async function createWebviewManager(host) { if (!target) { return; } - assertIsDefined(target.contentDocument).execCommand(data); + target.contentDocument.execCommand(data); }); trackFocus({ @@ -836,13 +784,18 @@ export async function createWebviewManager(host) { onBlur: () => host.postMessage('did-blur') }); - (/** @type {any} */ (window))[vscodePostMessageFuncName] = (/** @type {string} */ command, /** @type {any} */ data) => { + (/** @type {any} */ (window))[vscodePostMessageFuncName] = (command, data, transfer) => { switch (command) { case 'onmessage': case 'do-update-state': host.postMessage(command, data); break; } + if (e.defaultPrevented) { + // Extension code has already handled this event + return; + } + }; // signal ready From a65d55e9c48f7629eae430cd924f357b6aff8236 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 15:59:58 -0700 Subject: [PATCH 172/270] Strict null fixes in webview --- .../contrib/webview/browser/pre/main.js | 124 +++++++++++++----- 1 file changed, 88 insertions(+), 36 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 7e429b1969b..a5faa4d3316 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check +/// + /** * @typedef {{ * postMessage: (channel: string, data?: any) => void, - * onMessage: (channel: string, handler: any) => void, + * onMessage: (channel: string, handler: (event: MessageEvent, data: any) => void) => void, * focusIframeOnCreate?: boolean, * ready?: Promise, * onIframeLoaded?: (iframe: HTMLIFrameElement) => void, @@ -56,6 +58,18 @@ const getPendingFrame = () => { return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame')); }; +/** + * @template T + * @param {T | undefined | null} obj + * @return {T} + */ +function assertIsDefined(obj) { + if (typeof obj === 'undefined' || obj === null) { + throw new Error('Found unexpected null'); + } + return obj; +} + const vscodePostMessageFuncName = '__vscode_post_message__'; const defaultStyles = document.createElement('style'); @@ -202,6 +216,9 @@ const workerReady = new Promise(async (resolve, reject) => { async registration => { await navigator.serviceWorker.ready; + /** + * @param {MessageEvent} event + */ const versionHandler = (event) => { if (event.data.channel !== 'version') { return; @@ -218,7 +235,7 @@ const workerReady = new Promise(async (resolve, reject) => { } }; navigator.serviceWorker.addEventListener('message', versionHandler); - registration.active.postMessage({ channel: 'version' }); + assertIsDefined(registration.active).postMessage({ channel: 'version' }); }, error => { reject(new Error(`Could not register service workers: ${error}.`)); @@ -231,6 +248,7 @@ const workerReady = new Promise(async (resolve, reject) => { export async function createWebviewManager(host) { // state let firstLoad = true; + /** @type {any} */ let loadTimeout; let styleVersion = 0; @@ -241,7 +259,7 @@ export async function createWebviewManager(host) { /** @type {number | undefined} */ initialScrollProgress: undefined, - /** @type {{ [key: string]: string }} */ + /** @type {{ [key: string]: string } | undefined} */ styles: undefined, /** @type {string | undefined} */ @@ -253,13 +271,13 @@ export async function createWebviewManager(host) { host.onMessage('did-load-resource', (_event, data) => { navigator.serviceWorker.ready.then(registration => { - registration.active.postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); + assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); }); }); host.onMessage('did-load-localhost', (_event, data) => { navigator.serviceWorker.ready.then(registration => { - registration.active.postMessage({ channel: 'did-load-localhost', data }); + assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data }); }); }); @@ -282,7 +300,9 @@ export async function createWebviewManager(host) { if (body) { body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast'); - body.classList.add(initData.activeTheme); + if (initData.activeTheme) { + body.classList.add(initData.activeTheme); + } body.dataset.vscodeThemeKind = initData.activeTheme; body.dataset.vscodeThemeName = initData.themeName || ''; @@ -435,6 +455,9 @@ export async function createWebviewManager(host) { let isHandlingScroll = false; + /** + * @param {WheelEvent} event + */ const handleWheel = (event) => { if (isHandlingScroll) { return; @@ -450,15 +473,21 @@ export async function createWebviewManager(host) { }); }; + /** + * @param {Event} event + */ const handleInnerScroll = (event) => { - if (!event.target || !event.target.body) { - return; - } if (isHandlingScroll) { return; } - const progress = event.currentTarget.scrollY / event.target.body.clientHeight; + const target = /** @type {HTMLDocument | null} */ (event.target); + const currentTarget = /** @type {Window | null} */ (event.currentTarget); + if (!target || !currentTarget || !target.body) { + return; + } + + const progress = currentTarget.scrollY / target.body.clientHeight; if (isNaN(progress)) { return; } @@ -475,6 +504,19 @@ export async function createWebviewManager(host) { }; /** + * @typedef {{ + * contents: string; + * options: { + * readonly allowScripts: boolean; + * readonly allowMultipleAPIAcquire: boolean; + * } + * state: any; + * resourceEndpoint: string; + * }} ContentUpdateData + */ + + /** + * @param {ContentUpdateData} data * @return {string} */ function toContentHtml(data) { @@ -484,7 +526,10 @@ export async function createWebviewManager(host) { newDocument.querySelectorAll('a').forEach(a => { if (!a.title) { - a.title = a.getAttribute('href'); + const href = a.getAttribute('href'); + if (typeof href === 'string') { + a.title = href; + } } }); @@ -509,8 +554,11 @@ export async function createWebviewManager(host) { try { // Attempt to rewrite CSPs that hardcode old-style resource endpoint const endpointUrl = new URL(data.resourceEndpoint); - const newCsp = csp.getAttribute('content').replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); - csp.setAttribute('content', newCsp); + const cspContent = csp.getAttribute('content'); + if (cspContent) { + const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); + csp.setAttribute('content', newCsp); + } } catch (e) { console.error(`Could not rewrite csp: ${e}`); } @@ -563,7 +611,7 @@ export async function createWebviewManager(host) { // update iframe-contents let updateId = 0; - host.onMessage('content', async (_event, data) => { + host.onMessage('content', async (_event, /** @type {ContentUpdateData} */ data) => { const currentUpdateId = ++updateId; try { @@ -586,18 +634,19 @@ export async function createWebviewManager(host) { const frame = getActiveFrame(); const wasFirstLoad = firstLoad; // keep current scrollY around and use later + /** @type {(body: HTMLElement, window: Window) => void} */ let setInitialScrollPosition; if (firstLoad) { firstLoad = false; setInitialScrollPosition = (body, window) => { - if (!isNaN(initData.initialScrollProgress)) { + if (typeof initData.initialScrollProgress === 'number' && !isNaN(initData.initialScrollProgress)) { if (window.scrollY === 0) { window.scroll(0, body.clientHeight * initData.initialScrollProgress); } } }; } else { - const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentWindow.scrollY : 0; + const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? assertIsDefined(frame.contentWindow).scrollY : 0; setInitialScrollPosition = (body, window) => { if (window.scrollY === 0) { window.scroll(0, scrollY); @@ -656,15 +705,16 @@ export async function createWebviewManager(host) { return; } - if (newFrame.contentDocument.readyState !== 'loading') { + const contentDocument = assertIsDefined(newFrame.contentDocument); + if (contentDocument.readyState !== 'loading') { clearInterval(interval); - onFrameLoaded(newFrame.contentDocument); + onFrameLoaded(contentDocument); } }, 10); } else { - newFrame.contentWindow.addEventListener('DOMContentLoaded', e => { + assertIsDefined(newFrame.contentWindow).addEventListener('DOMContentLoaded', e => { const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; - onFrameLoaded(contentDocument); + onFrameLoaded(assertIsDefined(contentDocument)); }); } @@ -692,7 +742,7 @@ export async function createWebviewManager(host) { newFrame.setAttribute('id', 'active-frame'); newFrame.style.visibility = 'visible'; if (host.focusIframeOnCreate) { - newFrame.contentWindow.focus(); + assertIsDefined(newFrame.contentWindow).focus(); } contentWindow.addEventListener('scroll', handleInnerScroll); @@ -720,10 +770,12 @@ export async function createWebviewManager(host) { loadTimeout = setTimeout(() => { clearTimeout(loadTimeout); loadTimeout = undefined; - onLoad(newFrame.contentDocument, newFrame.contentWindow); + onLoad(assertIsDefined(newFrame.contentDocument), assertIsDefined(newFrame.contentWindow)); }, 200); - newFrame.contentWindow.addEventListener('load', function (e) { + const contentWindow = assertIsDefined(newFrame.contentWindow); + + contentWindow.addEventListener('load', function (e) { const contentDocument = /** @type {Document} */ (e.target); if (loadTimeout) { @@ -734,11 +786,16 @@ export async function createWebviewManager(host) { }); // Bubble out various events - newFrame.contentWindow.addEventListener('click', handleInnerClick); - newFrame.contentWindow.addEventListener('auxclick', handleAuxClick); - newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); - newFrame.contentWindow.addEventListener('keyup', handleInnerUp); - newFrame.contentWindow.addEventListener('contextmenu', e => { + contentWindow.addEventListener('click', handleInnerClick); + contentWindow.addEventListener('auxclick', handleAuxClick); + contentWindow.addEventListener('keydown', handleInnerKeydown); + contentWindow.addEventListener('keyup', handleInnerUp); + contentWindow.addEventListener('contextmenu', e => { + if (e.defaultPrevented) { + // Extension code has already handled this event + return; + } + e.preventDefault(); host.postMessage('did-context-menu', { clientX: e.clientX, @@ -760,7 +817,7 @@ export async function createWebviewManager(host) { if (!pending) { const target = getActiveFrame(); if (target) { - target.contentWindow.postMessage(data.message, '*', data.transfer); + assertIsDefined(target.contentWindow).postMessage(data.message, '*', data.transfer); return; } } @@ -776,7 +833,7 @@ export async function createWebviewManager(host) { if (!target) { return; } - target.contentDocument.execCommand(data); + assertIsDefined(target.contentDocument).execCommand(data); }); trackFocus({ @@ -784,18 +841,13 @@ export async function createWebviewManager(host) { onBlur: () => host.postMessage('did-blur') }); - (/** @type {any} */ (window))[vscodePostMessageFuncName] = (command, data, transfer) => { + (/** @type {any} */ (window))[vscodePostMessageFuncName] = (/** @type {string} */ command, /** @type {any} */ data) => { switch (command) { case 'onmessage': case 'do-update-state': host.postMessage(command, data); break; } - if (e.defaultPrevented) { - // Extension code has already handled this event - return; - } - }; // signal ready From 4fcc2720826658c53bf807a06a92a35eeded04ff Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Wed, 19 May 2021 16:01:20 -0700 Subject: [PATCH 173/270] use custom shield dialog #wt --- src/vs/workbench/services/workspaces/common/workspaceTrust.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index c49d8f6fade..7f134cebfa1 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; import { splitName } from 'vs/base/common/labels'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -389,6 +390,7 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Are you sure you want to open these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Window"), localize('cancel', "Cancel")], { detail: localize('openLooseFileDetails', "You are trying to open untrusted files into the current window which is trusted. How would you like to continue?"), cancelId: 2, + custom: { icon: Codicon.shield } }); switch (result.choice) { From 40592a274c9f6dc24c00819099b2103962b0f092 Mon Sep 17 00:00:00 2001 From: rebornix Date: Wed, 19 May 2021 16:13:29 -0700 Subject: [PATCH 174/270] fix test suite. --- src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 04da8ba66b7..7f474ee7440 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -177,7 +177,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic const viewContext = new ViewContext(new NotebookOptions(instantiationService.get(IConfigurationService)), new NotebookEventDispatcher()); const viewModel: NotebookViewModel = instantiationService.createInstance(NotebookViewModel, viewType, model.notebook, viewContext, null); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, viewContext); cellList.attachViewModel(viewModel); const listViewInfoAccessor = new ListViewInfoAccessor(cellList); @@ -275,7 +275,7 @@ export async function withTestNotebook(cells: [source: string, lang: st return res; } -export function createNotebookCellList(instantiationService: TestInstantiationService) { +export function createNotebookCellList(instantiationService: TestInstantiationService, viewContext?: ViewContext) { const delegate: IListVirtualDelegate = { getHeight(element: CellViewModel) { return element.getHeight(17); }, getTemplateId() { return 'template'; } @@ -293,6 +293,7 @@ export function createNotebookCellList(instantiationService: TestInstantiationSe 'NotebookCellList', DOM.$('container'), DOM.$('body'), + viewContext ?? new ViewContext(new NotebookOptions(instantiationService.get(IConfigurationService)), new NotebookEventDispatcher()), delegate, [renderer], instantiationService.get(IContextKeyService), From 39d1a94e86f752f72fff2a73565376e840c32e7b Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 16:46:06 -0700 Subject: [PATCH 175/270] Allow loading svgs from xhr Fixes #124124 In a webview, if a svg is loaded from localhost, it is intercepted by the service worker. When this happens in non-remote cases, we end up using `fetch` to hit the network. This causes the `.frame` property on the request `details` to become `undefined`, which causes us to block the request because we don't know it comes from a webview The fix is to allow svgs to be loaded if they are from xhr requests --- src/vs/platform/windows/electron-main/window.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index 12c49860d48..472c7ca2ee6 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -455,7 +455,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { const uri = URI.parse(details.url); if (uri.path.endsWith('.svg')) { const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme) || uri.path.includes(Schemas.vscodeRemoteResource); - if (!isSafeResourceUrl) { + if (!isSafeResourceUrl && details.resourceType !== 'xhr') { const isSafeContext = isSafeFrame(details.frame); return callback({ cancel: !isSafeContext }); } @@ -483,7 +483,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // remote extension schemes have the following format // http://127.0.0.1:/vscode-remote-resource?path= if (!uri.path.includes(Schemas.vscodeRemoteResource) && contentTypes.some(contentType => contentType.toLowerCase().includes('image/svg'))) { - const isSafeContext = isSafeFrame(details.frame); + const isSafeContext = details.resourceType === 'xhr' || isSafeFrame(details.frame); return callback({ cancel: !isSafeContext }); } } From b3ed595dde705c83648f8b963485fc5a30eea86e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 16:49:14 -0700 Subject: [PATCH 176/270] Extract isRequestFromSafeContext --- src/vs/platform/windows/electron-main/window.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index 472c7ca2ee6..c597738aa69 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -451,13 +451,16 @@ export class CodeWindow extends Disposable implements ICodeWindow { return false; }; + const isRequestFromSafeContext = (details: Electron.OnBeforeRequestListenerDetails | Electron.OnHeadersReceivedListenerDetails): boolean => { + return details.resourceType === 'xhr' || isSafeFrame(details.frame); + }; + this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => { const uri = URI.parse(details.url); if (uri.path.endsWith('.svg')) { const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme) || uri.path.includes(Schemas.vscodeRemoteResource); - if (!isSafeResourceUrl && details.resourceType !== 'xhr') { - const isSafeContext = isSafeFrame(details.frame); - return callback({ cancel: !isSafeContext }); + if (!isSafeResourceUrl) { + return callback({ cancel: !isRequestFromSafeContext(details) }); } } @@ -483,8 +486,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // remote extension schemes have the following format // http://127.0.0.1:/vscode-remote-resource?path= if (!uri.path.includes(Schemas.vscodeRemoteResource) && contentTypes.some(contentType => contentType.toLowerCase().includes('image/svg'))) { - const isSafeContext = details.resourceType === 'xhr' || isSafeFrame(details.frame); - return callback({ cancel: !isSafeContext }); + return callback({ cancel: !isRequestFromSafeContext(details) }); } } From 1ecba0426a9450188871a362e4542564be0cf920 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 17:01:23 -0700 Subject: [PATCH 177/270] Make `reload webviews` support webview-views Fixes #124212 This also makes the command run against non-visible webviews --- .../contrib/webview/browser/webview.ts | 5 +++++ .../contrib/webview/browser/webviewService.ts | 19 +++++++++++++++---- .../electron-browser/webviewService.ts | 4 ++-- .../webviewPanel/browser/webviewCommands.ts | 10 ++++------ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 6235a5f49bc..3dd09f3048f 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -32,6 +32,11 @@ export interface IWebviewService { */ readonly activeWebview: Webview | undefined; + /** + * All webviews. + */ + readonly webviews: Iterable; + /** * Fired when the currently focused webview changes. */ diff --git a/src/vs/workbench/contrib/webview/browser/webviewService.ts b/src/vs/workbench/contrib/webview/browser/webviewService.ts index 25e922e271d..d99b5796ae6 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewService.ts @@ -34,6 +34,12 @@ export class WebviewService extends Disposable implements IWebviewService { } } + private _webviews = new Set(); + + public get webviews(): Iterable { + return this._webviews.values(); + } + private readonly _onDidChangeActiveWebview = this._register(new Emitter()); public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event; @@ -44,7 +50,7 @@ export class WebviewService extends Disposable implements IWebviewService { extension: WebviewExtensionDescription | undefined, ): WebviewElement { const webview = this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider); - this.addWebviewListeners(webview); + this.registerNewWebview(webview); return webview; } @@ -55,11 +61,13 @@ export class WebviewService extends Disposable implements IWebviewService { extension: WebviewExtensionDescription | undefined, ): WebviewOverlay { const webview = this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension); - this.addWebviewListeners(webview); + this.registerNewWebview(webview); return webview; } - protected addWebviewListeners(webview: Webview) { + protected registerNewWebview(webview: Webview) { + this._webviews.add(webview); + webview.onDidFocus(() => { this.updateActiveWebview(webview); }); @@ -71,6 +79,9 @@ export class WebviewService extends Disposable implements IWebviewService { }; webview.onDidBlur(onBlur); - webview.onDidDispose(onBlur); + webview.onDidDispose(() => { + onBlur(); + this._webviews.delete(webview); + }); } } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts index ffb4870d5d4..f1603eebd35 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts @@ -28,7 +28,7 @@ export class ElectronWebviewService extends WebviewService { ): WebviewElement { const useIframes = this._configService.getValue('webview.experimental.useIframes') ?? !options.enableFindWidget; const webview = this._instantiationService.createInstance(useIframes ? ElectronIframeWebview : ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider); - this.addWebviewListeners(webview); + this.registerNewWebview(webview); return webview; } @@ -39,7 +39,7 @@ export class ElectronWebviewService extends WebviewService { extension: WebviewExtensionDescription | undefined, ): WebviewOverlay { const webview = this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension); - this.addWebviewListeners(webview); + this.registerNewWebview(webview); return webview; } } diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewCommands.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewCommands.ts index ee06e2a50d3..2a045adb649 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewCommands.ts @@ -11,7 +11,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_ENABLED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_ENABLED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewEditor } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditor'; import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -118,11 +118,9 @@ export class ReloadWebviewAction extends Action2 { } public async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - for (const editor of editorService.visibleEditors) { - if (editor instanceof WebviewInput) { - editor.webview.reload(); - } + const webviewService = accessor.get(IWebviewService); + for (const webview of webviewService.webviews) { + webview.reload(); } } } From 46a1ca78249d282976ae7964494203f23fad1449 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 19 May 2021 17:07:49 -0700 Subject: [PATCH 178/270] notebook: initial renderer communication --- src/vs/base/common/event.ts | 12 ++-- src/vs/base/test/common/event.test.ts | 10 +++ src/vs/vscode.proposed.d.ts | 38 ++++++++++ .../api/browser/extensionHost.contribution.ts | 1 + .../browser/mainThreadNotebookRenderers.ts | 29 ++++++++ .../workbench/api/common/extHost.api.impl.ts | 6 ++ .../workbench/api/common/extHost.protocol.ts | 10 +++ .../api/common/extHostNotebookRenderers.ts | 56 +++++++++++++++ .../browser/diff/notebookTextDiffEditor.ts | 4 +- .../notebook/browser/extensionPoint.ts | 16 +++++ .../notebook/browser/notebook.contribution.ts | 3 + .../notebook/browser/notebookEditorWidget.ts | 10 ++- .../notebookRendererMessagingServiceImpl.ts | 70 +++++++++++++++++++ .../notebook/browser/notebookServiceImpl.ts | 3 +- .../view/renderers/backLayerWebView.ts | 31 ++++++++ .../browser/view/renderers/webviewPreloads.ts | 48 +++++++++---- .../contrib/notebook/common/notebookCommon.ts | 3 + .../notebook/common/notebookOutputRenderer.ts | 14 +++- .../notebookRendererMessagingService.ts | 44 ++++++++++++ .../notebookRendererMessagingService.test.ts | 59 ++++++++++++++++ .../extensions/common/extensionsRegistry.ts | 5 ++ 21 files changed, 444 insertions(+), 28 deletions(-) create mode 100644 src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts create mode 100644 src/vs/workbench/api/common/extHostNotebookRenderers.ts create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts create mode 100644 src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts create mode 100644 src/vs/workbench/contrib/notebook/test/notebookRendererMessagingService.test.ts diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 4dbb96ccdb7..d992d0abe8a 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -645,13 +645,13 @@ export class Emitter { } dispose() { - this._listeners?.clear(); - this._deliveryQueue?.clear(); - if (this._options?.onLastListenerRemove) { - this._options.onLastListenerRemove(); + if (!this._disposed) { + this._disposed = true; + this._listeners?.clear(); + this._deliveryQueue?.clear(); + this._options?.onLastListenerRemove?.(); + this._leakageMon?.dispose(); } - this._leakageMon?.dispose(); - this._disposed = true; } } diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index a0f55cd405c..3fea2b43b4e 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -894,4 +894,14 @@ suite('Event utils', () => { listener.dispose(); }); + test('dispose is reentrant', () => { + const emitter = new Emitter({ + onLastListenerRemove: () => { + emitter.dispose(); + } + }); + + const listener = emitter.event(() => undefined); + listener.dispose(); // should not crash + }); }); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 16b6b7e85fd..67845179a46 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2150,6 +2150,44 @@ declare module 'vscode' { //#endregion + //#region @connor4312 - notebook messaging: https://github.com/microsoft/vscode/issues/123601 + + /** + * Renderer messaging is used to communicate with a single renderer. It's + * returned from {@link notebook.createRendererMessaging}. + */ + export interface NotebookRendererMessaging { + /** + * Events that fires when a message is received from a renderer. + */ + messageHandler?: (editor: NotebookEditor, message: TReceive) => void; + + /** + * Sends a message to the renderer. + * @param message Message to send + */ + postMessage(message: TSend): void; + + /** + * Disposes of the messaging instance and unsubscribes from messages. + */ + dispose(): void; + } + + export namespace notebook { + /** + * Creates a new messaging instance used to communicate with a specific + * renderer. The renderer only has access to messaging if `requiresMessaging` + * is set in its contribution. + * + * @see https://github.com/microsoft/vscode/issues/123601 + * @param rendererId The renderer ID to communicate with + */ + export function createRendererMessaging(rendererId: string): NotebookRendererMessaging; + } + + //#endregion + //#region @eamodio - timeline: https://github.com/microsoft/vscode/issues/84297 export class TimelineItem { diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 524697c3426..427e98ba553 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -65,6 +65,7 @@ import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadNotebookKernels'; import './mainThreadNotebookDocumentsAndEditors'; +import './mainThreadNotebookRenderers'; import './mainThreadTask'; import './mainThreadLabelService'; import './mainThreadTunnelService'; diff --git a/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts b/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts new file mode 100644 index 00000000000..0883f85bc29 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ExtHostContext, ExtHostNotebookRenderersShape, IExtHostContext, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; + +@extHostNamedCustomer(MainContext.MainThreadNotebookRenderers) +export class MainThreadNotebookRenderers extends Disposable implements MainThreadNotebookRenderersShape { + private readonly proxy: ExtHostNotebookRenderersShape; + + constructor( + extHostContext: IExtHostContext, + @INotebookRendererMessagingService private readonly messaging: INotebookRendererMessagingService, + ) { + super(); + this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookRenderers); + this._register(messaging.onShouldPostMessage(e => { + this.proxy.$postRendererMessage(e.editorId, e.rendererId, e.message); + })); + } + + $postMessage(rendererId: string, message: unknown): void { + this.messaging.fireDidReceiveMessage(rendererId, message); + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 71b0809eb0f..5a605ced71d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -87,6 +87,7 @@ import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; import { RemoteTrustOption } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; +import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -146,6 +147,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostLogService, extensionStoragePaths)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostLogService)); + const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, { ...initData.environment, remote: initData.remote })); @@ -1081,6 +1083,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.createNotebookEditorDecorationType(options); }, + createRendererMessaging(rendererId) { + checkProposedApiEnabled(extension); + return extHostNotebookRenderers.createRendererMessaging(rendererId); + }, onDidChangeNotebookDocumentMetadata(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookDocumentMetadata(listener, thisArgs, disposables); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 98368960f85..68dedb83461 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -916,6 +916,10 @@ export interface MainThreadNotebookKernelsShape extends IDisposable { $updateNotebookPriority(handle: number, uri: UriComponents, value: number | undefined): void; } +export interface MainThreadNotebookRenderersShape extends IDisposable { + $postMessage(rendererId: string, message: unknown): void; +} + export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise; $unregisterUriHandler(handle: number): Promise; @@ -1917,6 +1921,10 @@ export interface ExtHostNotebookShape extends ExtHostNotebookDocumentsAndEditors $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise; } +export interface ExtHostNotebookRenderersShape { + $postRendererMessage(editorId: string, rendererId: string, message: unknown): void; +} + export interface ExtHostNotebookDocumentsAndEditorsShape { $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; } @@ -2070,6 +2078,7 @@ export const MainContext = { MainThreadNotebookDocuments: createMainId('MainThreadNotebookDocumentsShape'), MainThreadNotebookEditors: createMainId('MainThreadNotebookEditorsShape'), MainThreadNotebookKernels: createMainId('MainThreadNotebookKernels'), + MainThreadNotebookRenderers: createMainId('MainThreadNotebookRenderers'), MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline'), @@ -2117,6 +2126,7 @@ export const ExtHostContext = { ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), ExtHostNotebookKernels: createMainId('ExtHostNotebookKernels'), + ExtHostNotebookRenderers: createMainId('ExtHostNotebookRenderers'), ExtHostTheming: createMainId('ExtHostTheming'), ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostNotebookRenderers.ts b/src/vs/workbench/api/common/extHostNotebookRenderers.ts new file mode 100644 index 00000000000..0d175f5c561 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookRenderers.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { ExtHostNotebookRenderersShape, IMainContext, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import * as vscode from 'vscode'; + +export class ExtHostNotebookRenderers implements ExtHostNotebookRenderersShape { + private readonly _rendererMessageEmitters = new Map>(); + private readonly proxy: MainThreadNotebookRenderersShape; + + constructor(mainContext: IMainContext, private readonly _extHostNotebook: ExtHostNotebookController) { + this.proxy = mainContext.getProxy(MainContext.MainThreadNotebookRenderers); + } + + public $postRendererMessage(editorId: string, rendererId: string, message: unknown): void { + const editor = this._extHostNotebook.getEditorById(editorId); + if (!editor) { + return; + } + + this._rendererMessageEmitters.get(rendererId)?.fire({ document: editor.apiEditor, data: message }); + } + + public createRendererMessaging(rendererId: string): vscode.NotebookRendererMessaging { + const emitter = this.getOrCreateEmitterFor(rendererId); + const listener = emitter.event(e => messaging.messageHandler?.(e.document, e.data)); + const messaging: vscode.NotebookRendererMessaging = { + dispose: () => listener.dispose(), + postMessage: message => this.proxy.$postMessage(rendererId, message), + }; + + return messaging; + } + + private getOrCreateEmitterFor(rendererId: string) { + let emitter = this._rendererMessageEmitters.get(rendererId); + if (emitter) { + return emitter; + } + + emitter = new Emitter({ + onLastListenerRemove: () => { + emitter?.dispose(); + this._rendererMessageEmitters.delete(rendererId); + } + }); + + this._rendererMessageEmitters.set(rendererId, emitter); + + return emitter; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 9db388ec10e..d7b4f046c7f 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -378,7 +378,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._modifiedWebview.dispose(); } - this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions()) as BackLayerWebView; + this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions(), undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); await this._modifiedWebview.createWebview(); @@ -391,7 +391,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._originalWebview.dispose(); } - this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions()) as BackLayerWebView; + this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions(), undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); await this._originalWebview.createWebview(); diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index 959e70bf586..e925a6fdf98 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -30,6 +30,7 @@ namespace NotebookRendererContribution { export const entrypoint = 'entrypoint'; export const hardDependencies = 'dependencies'; export const optionalDependencies = 'optionalDependencies'; + export const requiresMessaging = 'requiresMessaging'; } export interface INotebookRendererContribution { @@ -40,6 +41,7 @@ export interface INotebookRendererContribution { readonly [NotebookRendererContribution.entrypoint]: NotebookRendererEntrypoint; readonly [NotebookRendererContribution.hardDependencies]: readonly string[]; readonly [NotebookRendererContribution.optionalDependencies]: readonly string[]; + readonly [NotebookRendererContribution.requiresMessaging]: boolean | 'optional' | undefined; } const notebookProviderContribution: IJSONSchema = { @@ -164,6 +166,20 @@ const notebookRendererContribution: IJSONSchema = { items: { type: 'string' }, markdownDescription: nls.localize('contributes.notebook.renderer.optionalDependencies', 'List of soft kernel dependencies the renderer can make use of. If any of the dependencies are present in the `NotebookKernel.preloads`, the renderer will be preferred over renderers that don\'t interact with the kernel.'), }, + [NotebookRendererContribution.requiresMessaging]: { + default: false, + enum: [ + true, + false, + 'optional' + ], + enumDescriptions: [ + nls.localize('contributes.notebook.renderer.requiresMessaging.true', 'Messaging is required. The renderer will only be used when it\'s part of an extension that can be run in an extension host.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.optional', 'The renderer is better with messaging available, but it\'s not requried.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.false', 'The renderer does not require messaging.'), + ], + description: nls.localize('contributes.notebook.renderer.requiresMessaging', 'Defines how and if the renderer needs to communicate with an extension host, via `createRendererMessaging`. Renderers with stronger messaging requirements may not work in all environments.'), + }, } } }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index f0051ae83d7..34d93102f1f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -82,6 +82,8 @@ import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransfo import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; /*--------------------------------------------------------------------------------------------- */ @@ -544,6 +546,7 @@ registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverServ registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); registerSingleton(INotebookEditorService, NotebookEditorWidgetService, true); registerSingleton(INotebookKernelService, NotebookKernelService, true); +registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, true); const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 077b33d94c3..a4c92f90a75 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -42,7 +42,7 @@ import { IEditorMemento } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_ID, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_ID, NOTEBOOK_OUTPUT_FOCUSED, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookDecorationCSSRules, NotebookRefCountedStyleSheet } from 'vs/workbench/contrib/notebook/browser/notebookEditorDecorations'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookEditorKernelManager } from 'vs/workbench/contrib/notebook/browser/notebookEditorKernelManager'; @@ -72,6 +72,7 @@ import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/notebookEditorToolbar'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; const $ = DOM.$; @@ -316,6 +317,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IAccessibilityService accessibilityService: IAccessibilityService, + @INotebookRendererMessagingService private readonly notebookRendererMessaging: INotebookRendererMessagingService, @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -1108,7 +1110,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private async _createWebview(id: string, resource: URI): Promise { - this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeWebviewOptions()); + this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeWebviewOptions(), this.notebookRendererMessaging.getScoped(this._uuid)); this._webview.element.style.width = '100%'; // attach the webview container to the DOM tree first @@ -2259,6 +2261,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } + if (output.type === RenderOutputType.Extension) { + this.notebookRendererMessaging.prepare(output.renderer.id); + } + const cellTop = this._list.getAbsoluteTopOfElement(cell); if (!this._webview.insetMapping.has(output.source)) { await this._webview.createOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts new file mode 100644 index 00000000000..6524ebb4224 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { INotebookRendererMessagingService, IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +type MessageToSend = { editorId: string; rendererId: string; message: unknown }; + +export class NotebookRendererMessagingService implements INotebookRendererMessagingService { + declare _serviceBrand: undefined; + /** + * Activation promises. Maps renderer IDs to a queue of messages that should + * be sent once activation finishes, or undefined if activation is complete. + */ + private readonly activations = new Map(); + private readonly receiveMessageEmitter = new Emitter<{ rendererId: string, message: unknown }>(); + public readonly onDidReceiveMessage = this.receiveMessageEmitter.event; + private readonly postMessageEmitter = new Emitter(); + public readonly onShouldPostMessage = this.postMessageEmitter.event; + + constructor(@IExtensionService private readonly extensionService: IExtensionService) { } + + /** @inheritdoc */ + public fireDidReceiveMessage(rendererId: string, message: unknown): void { + this.receiveMessageEmitter.fire({ rendererId, message }); + } + + /** @inheritdoc */ + public prepare(rendererId: string) { + if (this.activations.has(rendererId)) { + return; + } + + const queue: MessageToSend[] = []; + this.activations.set(rendererId, queue); + + this.extensionService.activateByEvent(`onRenderer:${rendererId}`).then(() => { + for (const message of queue) { + this.postMessageEmitter.fire(message); + } + + this.activations.set(rendererId, undefined); + }); + } + + /** @inheritdoc */ + public getScoped(editorId: string): IScopedRendererMessaging { + return { + onDidReceiveMessage: this.onDidReceiveMessage, + postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message), + }; + } + + private postMessage(editorId: string, rendererId: string, message: unknown): void { + if (!this.activations.has(rendererId)) { + this.prepare(rendererId); + } + + const activation = this.activations.get(rendererId); + const toSend = { rendererId, editorId, message }; + if (activation === undefined) { + this.postMessageEmitter.fire(toSend); + } else { + activation.push(toSend); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 05db784c920..90c4f8bc43e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -359,7 +359,7 @@ export class NotebookService extends Disposable implements INotebookService { continue; } - this._notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({ + this._notebookRenderersInfoStore.add(this._instantiationService.createInstance(NotebookOutputRendererInfo, { id, extension: extension.description, entrypoint: notebookContribution.entrypoint, @@ -367,6 +367,7 @@ export class NotebookService extends Disposable implements INotebookService { mimeTypes: notebookContribution.mimeTypes || [], dependencies: notebookContribution.dependencies, optionalDependencies: notebookContribution.optionalDependencies, + requiresMessaging: notebookContribution.requiresMessaging, })); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index a48496cf9e4..b8ff078934f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -30,6 +30,7 @@ import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebo import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { INotebookKernel, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewContentPurpose, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -286,6 +287,12 @@ export interface ICustomKernelMessage extends BaseToWebviewMessage { message: unknown; } +export interface ICustomRendererMessage extends BaseToWebviewMessage { + type: 'customRendererMessage'; + rendererId: string; + message: unknown; +} + export interface ICreateMarkdownMessage { type: 'createMarkdownPreview', cell: IMarkdownCellInitialization; @@ -349,6 +356,7 @@ export type FromWebviewMessage = | IScrollAckMessage | IBlurOutputMessage | ICustomKernelMessage + | ICustomRendererMessage | IClickedDataUrlMessage | IClickMarkdownPreviewMessage | IContextMenuMarkdownPreviewMessage @@ -377,6 +385,7 @@ export type ToWebviewMessage = | IUpdateControllerPreloadsMessage | IUpdateDecorationsMessage | ICustomKernelMessage + | ICustomRendererMessage | ICreateMarkdownMessage | IDeleteMarkdownMessage | IShowMarkdownMessage @@ -440,6 +449,7 @@ export class BackLayerWebView extends Disposable { rightMargin: number, runGutter: number, }, + private readonly rendererMessaging: IScopedRendererMessaging | undefined, @IWebviewService readonly webviewService: IWebviewService, @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, @@ -458,6 +468,17 @@ export class BackLayerWebView extends Disposable { this.element.style.height = '1400px'; this.element.style.position = 'absolute'; + + if (rendererMessaging) { + this._register(rendererMessaging.onDidReceiveMessage(evt => { + this._sendMessageToWebview({ + __vscode_notebook_message: true, + type: 'customRendererMessage', + rendererId: evt.rendererId, + message: evt.message + }); + })); + } } updateOptions(options: { @@ -766,6 +787,7 @@ export class BackLayerWebView extends Disposable { entrypoint, mimeTypes: renderer.mimeTypes, extends: renderer.extends, + messaging: !!renderer.messaging, }; }); } @@ -877,6 +899,10 @@ var requirejs = (function() { return; } + if (matchesScheme(link, Schemas.command)) { + console.warn('Command links are deprecated and will be removed, use messag passing instead: https://github.com/microsoft/vscode/issues/123601'); + } + if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto) || matchesScheme(link, Schemas.command)) { this.openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true, allowCommands: true }); @@ -1001,6 +1027,11 @@ var requirejs = (function() { this._onMessage.fire({ message: data.message }); break; } + case 'customRendererMessage': + { + this.rendererMessaging?.postMessage(data.rendererId, data.message); + break; + } case 'clickMarkdownPreview': { const cell = this.notebookEditor.getCellById(data.cellId); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index f70fb7c26c7..c05e38d2bc4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -124,19 +124,9 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend interface RendererContext { getState(): T | undefined; setState(newState: T): void; - getRenderer(id: string): any | undefined; - } - - function createRendererContext(rendererId: string): RendererContext { - return { - setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }), - getState: () => { - const state = vscode.getState(); - return typeof state === 'object' && state ? state[rendererId] as T : undefined; - }, - getRenderer: (id: string) => renderers.getRenderer(id), - }; + postMessage?(message: unknown): void; + onDidReceiveMessage?: Event; } interface ScriptModule { @@ -773,6 +763,9 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend case 'customKernelMessage': onDidReceiveKernelMessage.fire(event.data.message); break; + case 'customRendererMessage': + renderers.getRenderer(event.data.rendererId)?.receiveMessage(event.data.message); + break; case 'notebookStyles': const documentStyle = document.documentElement.style; @@ -804,6 +797,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend private readonly loadExtension: (id: string) => Promise, ) { } + private _onMessageEvent = createEmitter(); private _loadPromise: Promise | undefined; private _api: RendererApi | undefined; @@ -817,6 +811,29 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend return this._loadPromise; } + public receiveMessage(message: unknown) { + this._onMessageEvent.fire(message); + } + + private createRendererContext(): RendererContext { + const { id, messaging } = this.data; + const context: RendererContext = { + setState: newState => vscode.setState({ ...vscode.getState(), [id]: newState }), + getState: () => { + const state = vscode.getState(); + return typeof state === 'object' && state ? state[id] as T : undefined; + }, + getRenderer: (id: string) => renderers.getRenderer(id)?.api, + }; + + if (messaging) { + context.onDidReceiveMessage = this._onMessageEvent.event; + context.postMessage = message => postNotebookMessage('customRendererMessage', { rendererId: id, message }); + } + + return context; + } + /** Inner function cached in the _loadPromise(). */ private async _load() { const module = await runRenderScript(this.data.entrypoint, this.data.id); @@ -824,7 +841,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend return; } - const api = module.activate(createRendererContext(this.data.id)); + const api = module.activate(this.createRendererContext()); this._api = api; // Squash any errors extends errors. They won't prevent the renderer @@ -925,8 +942,8 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } } - public getRenderer(id: string): RendererApi | undefined { - return this._renderers.get(id)?.api; + public getRenderer(id: string) { + return this._renderers.get(id); } public async load(id: string) { @@ -1211,6 +1228,7 @@ export interface RendererMetadata { readonly entrypoint: string; readonly mimeTypes: readonly string[]; readonly extends: string | undefined; + readonly messaging: boolean; } export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly RendererMetadata[]) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index d8fcf0068cb..8eda48a57b3 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -131,6 +131,8 @@ export const enum NotebookRendererMatch { Never = 3, } +export type RendererMessagingSpec = true | false | 'optional'; + export interface INotebookRendererInfo { id: string; displayName: string; @@ -139,6 +141,7 @@ export interface INotebookRendererInfo { preloads: ReadonlyArray; extensionLocation: URI; extensionId: ExtensionIdentifier; + messaging: RendererMessagingSpec; readonly mimeTypes: readonly string[]; diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index 5025db38aeb..3ce2880b1d6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -8,7 +8,8 @@ import { Iterable } from 'vs/base/common/iterator'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { INotebookRendererInfo, NotebookRendererEntrypoint, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookRendererInfo, NotebookRendererEntrypoint, NotebookRendererMatch, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; class DependencyList { private readonly value: ReadonlySet; @@ -41,6 +42,7 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { readonly extensionId: ExtensionIdentifier; readonly hardDependencies: DependencyList; readonly optionalDependencies: DependencyList; + readonly messaging: RendererMessagingSpec; // todo: re-add preloads in pure renderer API readonly preloads: ReadonlyArray = []; @@ -55,7 +57,8 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { readonly extension: IExtensionDescription; readonly dependencies: readonly string[] | undefined; readonly optionalDependencies: readonly string[] | undefined; - }) { + readonly requiresMessaging: RendererMessagingSpec | undefined; + }, @IExtensionService public readonly extensions: IExtensionService) { this.id = descriptor.id; this.extensionId = descriptor.extension.identifier; this.extensionLocation = descriptor.extension.extensionLocation; @@ -72,6 +75,7 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern)); this.hardDependencies = new DependencyList(descriptor.dependencies ?? Iterable.empty()); this.optionalDependencies = new DependencyList(descriptor.optionalDependencies ?? Iterable.empty()); + this.messaging = descriptor.requiresMessaging ?? false; } get dependencies(): string[] { @@ -83,6 +87,12 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { return NotebookRendererMatch.Never; } + // todo@connor4312 this a no-op since extensions that can't run are never + // shared as a contribution + if (this.messaging === true && !this.extensions.getExtensionsStatus()[this.extensionId.value]) { + return NotebookRendererMatch.Never; + } + if (this.hardDependencies.defined) { return NotebookRendererMatch.WithHardKernelDependency; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts b/src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts new file mode 100644 index 00000000000..0f00b8f6af3 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const INotebookRendererMessagingService = createDecorator('INotebookRendererMessagingService'); + +export interface INotebookRendererMessagingService { + readonly _serviceBrand: undefined; + + /** + * Event that fires when a message should be posted to extension hosts. + */ + onShouldPostMessage: Event<{ editorId: string; rendererId: string; message: unknown }>; + + /** + * Prepares messaging for the given renderer ID. + */ + prepare(rendererId: string): void; + /** + * Gets messaging scoped for a specific editor. + */ + getScoped(editorId: string): IScopedRendererMessaging; + + /** + * Called when the main thread gets a message for a renderer. + */ + fireDidReceiveMessage(rendererId: string, message: unknown): void; +} + +export interface IScopedRendererMessaging { + /** + * Event that fires when a message is received. + */ + onDidReceiveMessage: Event<{ rendererId: string; message: unknown }>; + + /** + * Sends a message to an extension from a renderer. + */ + postMessage(rendererId: string, message: unknown): void; +} diff --git a/src/vs/workbench/contrib/notebook/test/notebookRendererMessagingService.test.ts b/src/vs/workbench/contrib/notebook/test/notebookRendererMessagingService.test.ts new file mode 100644 index 00000000000..b5c2774c766 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/notebookRendererMessagingService.test.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { stub } from 'sinon'; +import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl'; +import * as assert from 'assert'; +import { timeout } from 'vs/base/common/async'; + +suite('NotebookRendererMessaging', () => { + let extService: NullExtensionService; + let m: NotebookRendererMessagingService; + let sent: unknown[] = []; + let received: unknown[] = []; + + setup(() => { + sent = []; + extService = new NullExtensionService(); + m = new NotebookRendererMessagingService(extService); + m.onShouldPostMessage(e => sent.push(e)); + m.onDidReceiveMessage(e => received.push(e)); + }); + + test('activates on prepare', () => { + const activate = stub(extService, 'activateByEvent').returns(Promise.resolve()); + m.prepare('foo'); + m.prepare('foo'); + m.prepare('foo'); + + assert.deepStrictEqual(activate.args, [['onRenderer:foo']]); + }); + + test('buffers and then plays events', async () => { + stub(extService, 'activateByEvent').returns(Promise.resolve()); + + const scoped = m.getScoped('some-editor'); + scoped.postMessage('foo', 1); + scoped.postMessage('foo', 2); + assert.deepStrictEqual(sent, []); + + await timeout(0); + + const expected = [ + { editorId: 'some-editor', rendererId: 'foo', message: 1 }, + { editorId: 'some-editor', rendererId: 'foo', message: 2 } + ]; + + assert.deepStrictEqual(sent, expected); + + scoped.postMessage('foo', 3); + + assert.deepStrictEqual(sent, [ + ...expected, + { editorId: 'some-editor', rendererId: 'foo', message: 3 } + ]); + }); +}); diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index dca05b7ca6d..91eac5e38c3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -320,6 +320,11 @@ export const schema: IJSONSchema = { body: 'onAuthenticationRequest:${11:authenticationProviderId}', description: nls.localize('vscode.extension.activationEvents.onAuthenticationRequest', 'An activation event emitted whenever sessions are requested from the specified authentication provider.') }, + { + label: 'onRenderer', + description: nls.localize('vscode.extension.activationEvents.onRenderer', 'An activation event emitted whenever a notebook output renderer is used.'), + body: 'onRenderer:${11:rendererId}' + }, { label: '*', description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'), From 93be0a6fa03d1ab45f89dca251b7861e940f1992 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 17:14:29 -0700 Subject: [PATCH 179/270] Update simple service Missed adding this in the previous commit --- src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index e168d463051..c27cd8e4f63 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -12,7 +12,7 @@ import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnectio import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { isWindows } from 'vs/base/common/platform'; -import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, Webview, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { ITunnelProvider, ITunnelService, RemoteTunnel, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; @@ -270,6 +270,7 @@ class SimpleWebviewService implements IWebviewService { declare readonly _serviceBrand: undefined; readonly activeWebview = undefined; + readonly webviews: Webview[] = []; readonly onDidChangeActiveWebview = Event.None; createWebviewElement(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewElement { throw new Error('Method not implemented.'); } From 1ddc623e585abbc1348fe062651a3bb350e409d9 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 17:26:51 -0700 Subject: [PATCH 180/270] Simplify logic for webview resource uris (#123740) * Simplify logic for webview resource uris This change attempts to simplify the logic around webview resource uris by doing the following: - Hard code the resource origin. We always will be hitting a service worker for these paths so they don't need to be dynamic (although in the future we may want to pull them from `product.json`) This lets us remove these properties from the environment service - Move remote handling from the resource loader in `asWebviewUri`. - Remove the handling of http and https paths from the resource loader. I don't think these cases can be hit any longer (although I need to confirm this with more testing for the web case). Instead I added a check to `asWebviewUri` so that we return the original uri if a http(s) uri is passed in * Restore normalizeResourcePath We still need to convert between a remote uri and one that our remote file system can read * Fix test * Restore passing in remote on extension side * Remove only --- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../workbench/api/common/extHost.protocol.ts | 2 - .../workbench/api/common/extHostCodeInsets.ts | 6 +- .../api/common/extHostNotebookKernels.ts | 2 +- src/vs/workbench/api/common/extHostWebview.ts | 7 +- src/vs/workbench/api/common/shared/webview.ts | 40 +++++++++-- .../view/renderers/backLayerWebView.ts | 7 +- .../webview/browser/baseWebviewElement.ts | 46 ++++++------ .../webview/browser/pre/service-worker.js | 3 +- .../webview/browser/resourceLoading.ts | 70 +++++-------------- .../contrib/webview/browser/webviewElement.ts | 10 +-- .../electron-browser/webviewElement.ts | 9 +-- .../electron-sandbox/iframeWebviewElement.ts | 11 +-- .../gettingStarted/browser/gettingStarted.ts | 9 +-- .../environment/browser/environmentService.ts | 11 --- .../environment/common/environmentService.ts | 2 - .../electron-sandbox/environmentService.ts | 15 ---- .../browser/webWorkerExtensionHost.ts | 2 - .../extensions/common/remoteExtensionHost.ts | 4 +- .../localProcessExtensionHost.ts | 2 - .../test/browser/api/extHostWebview.test.ts | 63 +++++++++-------- 21 files changed, 131 insertions(+), 194 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 282ccda9f43..9bde67e9b9f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -147,7 +147,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostLogService)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); - const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, { ...initData.environment, remote: initData.remote })); + const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService)); const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostDiagnostics, extHostLogService, extHostApiDeprecation)); const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); @@ -160,7 +160,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); - const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, { ...initData.environment, remote: initData.remote }, extHostWorkspace, extHostLogService, extHostApiDeprecation)); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, { remote: initData.remote }, extHostWorkspace, extHostLogService, extHostApiDeprecation)); const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 98368960f85..4cb3d5e300d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -74,8 +74,6 @@ export interface IEnvironment { extensionTestsLocationURI?: URI; globalStorageHome: URI; workspaceStorageHome: URI; - webviewResourceRoot: string; - webviewCspSource: string; useHostProxy?: boolean; } diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 7db034cd81f..1ee7661d9fd 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -10,7 +10,7 @@ import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; import type * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; -import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; +import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { @@ -66,11 +66,11 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { private _options: vscode.WebviewOptions = Object.create(null); asWebviewUri(resource: vscode.Uri): vscode.Uri { - return asWebviewUri(that._initData, this._uuid, resource); + return asWebviewUri(this._uuid, resource, that._initData.remote.authority); } get cspSource(): string { - return that._initData.webviewCspSource; + return webviewGenericCspSource; } set options(value: vscode.WebviewOptions) { diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index f95d887f8f2..cc0be03859d 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -190,7 +190,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); }, asWebviewUri(uri: URI) { - return asWebviewUri({ ...that._initData.environment, remote: that._initData.remote }, String(handle), uri); + return asWebviewUri(String(handle), uri, that._initData.remote.authority); }, // --- priority updateNotebookAffinity(notebook, priority) { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index d7cd5f11644..b7bbde06834 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -12,7 +12,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { serializeWebviewMessage, deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; +import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; import * as extHostProtocol from './extHost.protocol'; @@ -69,12 +69,11 @@ export class ExtHostWebview implements vscode.Webview { public asWebviewUri(resource: vscode.Uri): vscode.Uri { this.#hasCalledAsWebviewUri = true; - return asWebviewUri(this.#initData, this.#handle, resource); + return asWebviewUri(this.#handle, resource, this.#initData.remote.authority); } public get cspSource(): string { - return this.#initData.webviewCspSource - .replace('{{uuid}}', this.#handle); + return webviewGenericCspSource; } public get html(): string { diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index f1de57db839..13ec3d02943 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -3,16 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import type * as vscode from 'vscode'; export interface WebviewInitData { - readonly isExtensionDevelopmentDebug: boolean; - readonly webviewResourceRoot: string; - readonly webviewCspSource: string; readonly remote: { readonly authority: string | undefined }; } +/** + * Location where we load resources from + * + * There endpoints can be hardcoded because we never expect to actually hit them. Instead these requests + * should always go to a service worker. + */ +export const webviewResourceOrigin = (id: string) => `https://${id}.vscode-webview-test.com`; + +export const webviewResourceRoot = (id: string) => `${webviewResourceOrigin(id)}/vscode-resource/{{resource}}`; + +export const webviewGenericCspSource = 'https://*.vscode-webview-test.com'; + /** * Construct a uri that can load resources inside a webview * @@ -20,16 +30,32 @@ export interface WebviewInitData { * we know where to load the resource from (remote or truly local): * * ```txt - * /remote-authority?/scheme/resource-authority/path... + * scheme/resource-authority/path... * ``` + * + * @param uuid Unique id of the webview. + * @param resource Uri of the resource to load. + * @param fromAuthority Optional remote authority that specifies where `resource` should be resolved from. */ export function asWebviewUri( - initData: WebviewInitData, uuid: string, resource: vscode.Uri, + fromAuthority: string | undefined ): vscode.Uri { - const uri = initData.webviewResourceRoot - .replace('{{resource}}', (initData.remote.authority ?? '') + '/' + resource.scheme + '/' + encodeURIComponent(resource.authority) + resource.path) + if (resource.scheme === Schemas.http || resource.scheme === Schemas.https) { + return resource; + } + + if (fromAuthority && resource.scheme === Schemas.file) { + resource = URI.from({ + scheme: Schemas.vscodeRemote, + authority: fromAuthority, + path: resource.path, + }); + } + + const uri = webviewResourceRoot(uuid) + .replace('{{resource}}', resource.scheme + '/' + encodeURIComponent(resource.authority) + resource.path) .replace('{{uuid}}', uuid); return URI.parse(uri).with({ fragment: resource.fragment, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 157cb43f9c2..a4c05e8813f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -766,12 +766,7 @@ export class BackLayerWebView extends Disposable { private asWebviewUri(uri: URI, fromExtension: URI | undefined) { const remoteAuthority = fromExtension?.scheme === Schemas.vscodeRemote ? fromExtension.authority : undefined; - return asWebviewUri({ - isExtensionDevelopmentDebug: this.environmentService.isExtensionDevelopment, - webviewCspSource: this.environmentService.webviewCspSource, - webviewResourceRoot: this.environmentService.webviewResourceRoot, - remote: { authority: remoteAuthority } - }, this.id, uri); + return asWebviewUri(this.id, uri, remoteAuthority); } postKernelMessage(message: any) { diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 7fabfa93909..1653f9bff32 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -23,9 +23,9 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; +import { asWebviewUri, webviewResourceOrigin } from 'vs/workbench/api/common/shared/webview'; import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/browser/resourceLoading'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { areWebviewContentOptionsEqual, WebviewContentOptions, WebviewExtensionDescription, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; @@ -100,7 +100,6 @@ export abstract class BaseWebview extends Disposable { private readonly _fileService: IFileService; private readonly _logService: ILogService; private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService; - private readonly _requestService: IRequestService; private readonly _telemetryService: ITelemetryService; private readonly _tunnelService: ITunnelService; protected readonly _environmentService: IWorkbenchEnvironmentService; @@ -122,7 +121,6 @@ export abstract class BaseWebview extends Disposable { menuService: IMenuService, notificationService: INotificationService, remoteAuthorityResolverService: IRemoteAuthorityResolverService, - requestService: IRequestService, telemetryService: ITelemetryService, tunnelService: ITunnelService, } @@ -133,7 +131,6 @@ export abstract class BaseWebview extends Disposable { this._fileService = services.fileService; this._logService = services.logService; this._remoteAuthorityResolverService = services.remoteAuthorityResolverService; - this._requestService = services.requestService; this._telemetryService = services.telemetryService; this._tunnelService = services.tunnelService; @@ -244,19 +241,19 @@ export abstract class BaseWebview extends Disposable { this._register(this.on(WebviewMessageChannels.loadResource, (entry: { id: number, path: string, query: string, ifNoneMatch?: string }) => { const rawPath = entry.path; - // ext-authority / scheme / path-authority / ...path - const match = rawPath.match(/^\/([^\/]*)\/([^\/]*)\/([^\/]*)(\/.+)$/); + // scheme / path-authority / ...path + const match = rawPath.match(/^\/([^\/]*)\/([^\/]*)(\/.+)$/); if (!match) { throw new Error('Could not parse resource url'); } - const [_, remoteAuthority, scheme, pathAuthority, paths] = match; + const [_, scheme, pathAuthority, paths] = match; const uri = URI.parse(`${scheme}://${decodeURIComponent(pathAuthority)}${paths}`).with({ query: decodeURIComponent(entry.query), }); - this.loadResource(entry.id, rawPath, uri, decodeURIComponent(remoteAuthority), entry.ifNoneMatch); + this.loadResource(entry.id, rawPath, uri, entry.ifNoneMatch); })); this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => { @@ -377,16 +374,28 @@ export abstract class BaseWebview extends Disposable { }); } - protected abstract get webviewResourceEndpoint(): string; + protected get webviewResourceOrigin(): string { + return webviewResourceOrigin(this.id); + } private rewriteVsCodeResourceUrls(value: string): string { - const remoteAuthority = this.extension?.location.scheme === Schemas.vscodeRemote ? this.extension.location.authority : ''; + const remoteAuthority = this.extension?.location.scheme === Schemas.vscodeRemote ? this.extension.location.authority : undefined; return value .replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => { - return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${remoteAuthority}/${scheme ?? 'file'}/${path}${endQuote}`; + const uri = URI.from({ + scheme: scheme || 'file', + path: path, + }); + const webviewUri = asWebviewUri(this.id, uri, remoteAuthority).toString(); + return `${startQuote}${webviewUri}${endQuote}`; }) .replace(/(["'])(?:vscode-webview-resource):(\/\/[^\s\/'"]+\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => { - return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${remoteAuthority}/${scheme ?? 'file'}/${path}${endQuote}`; + const uri = URI.from({ + scheme: scheme || 'file', + path: path, + }); + const webviewUri = asWebviewUri(this.id, uri, remoteAuthority).toString(); + return `${startQuote}${webviewUri}${endQuote}`; }); } @@ -433,7 +442,7 @@ export abstract class BaseWebview extends Disposable { contents: this.content.html, options: this.content.options, state: this.content.state, - resourceEndpoint: this.webviewResourceEndpoint, + resourceEndpoint: this.webviewResourceOrigin, ...this.extraContentOptions }); } @@ -512,15 +521,12 @@ export abstract class BaseWebview extends Disposable { } } - private async loadResource(id: number, requestPath: string, uri: URI, remoteAuthority: string | undefined, ifNoneMatch: string | undefined) { + private async loadResource(id: number, requestPath: string, uri: URI, ifNoneMatch: string | undefined) { try { - const remoteConnectionData = remoteAuthority ? this._remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null; - - const result = await loadLocalResource(uri, ifNoneMatch, { + const result = await loadLocalResource(uri, { + ifNoneMatch, roots: this.content.options.localResourceRoots || [], - remoteConnectionData, - remoteAuthority: remoteAuthority, - }, this._fileService, this._requestService, this._logService, this._resourceLoadingCts.token); + }, this._fileService, this._logService, this._resourceLoadingCts.token); switch (result.type) { case WebviewResourceResponse.Type.Success: diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 20576472cc5..2589f552df0 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -25,7 +25,7 @@ const resourceOrigin = searchParams.get('vscode-resource-origin') ?? sw.origin; /** * Root path for resources */ -const resourceRoot = rootPath + '/vscode-resource'; +const resourceRoot = '/vscode-resource'; const serviceWorkerFetchIgnoreSubdomain = searchParams.get('serviceWorkerFetchIgnoreSubdomain') ?? false; @@ -175,7 +175,6 @@ sw.addEventListener('message', async (event) => { sw.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); - if (serviceWorkerFetchIgnoreSubdomain && requestUrl.pathname.startsWith(resourceRoot + '/')) { // #121981 const ignoreFirstSubdomainRegex = /(.*):\/\/.*?\.(.*)/; diff --git a/src/vs/workbench/contrib/webview/browser/resourceLoading.ts b/src/vs/workbench/contrib/webview/browser/resourceLoading.ts index 79e68e5e6bf..5b6ebc58ac2 100644 --- a/src/vs/workbench/contrib/webview/browser/resourceLoading.ts +++ b/src/vs/workbench/contrib/webview/browser/resourceLoading.ts @@ -9,11 +9,8 @@ import { isUNC } from 'vs/base/common/extpath'; import { Schemas } from 'vs/base/common/network'; import { sep } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { IHeaders } from 'vs/base/parts/request/common/request'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; -import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { IRequestService } from 'vs/platform/request/common/request'; import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes'; export namespace WebviewResourceResponse { @@ -45,20 +42,17 @@ export namespace WebviewResourceResponse { export async function loadLocalResource( requestUri: URI, - ifNoneMatch: string | undefined, options: { + ifNoneMatch: string | undefined, roots: ReadonlyArray; - remoteConnectionData?: IRemoteConnectionData | null; - remoteAuthority: string | undefined; }, fileService: IFileService, - requestService: IRequestService, logService: ILogService, token: CancellationToken, ): Promise { logService.debug(`loadLocalResource - being. requestUri=${requestUri}`); - const resourceToLoad = getResourceToLoad(requestUri, options.roots, options.remoteAuthority); + const resourceToLoad = getResourceToLoad(requestUri, options.roots); logService.debug(`loadLocalResource - found resource to load. requestUri=${requestUri}, resourceToLoad=${resourceToLoad}`); @@ -68,33 +62,8 @@ export async function loadLocalResource( const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime - if (resourceToLoad.scheme === Schemas.http || resourceToLoad.scheme === Schemas.https) { - const headers: IHeaders = {}; - if (ifNoneMatch) { - headers['If-None-Match'] = ifNoneMatch; - } - - const response = await requestService.request({ - url: resourceToLoad.toString(true), - headers: headers - }, token); - - logService.debug(`loadLocalResource - Loaded over http(s). requestUri=${requestUri}, response=${response.res.statusCode}`); - - switch (response.res.statusCode) { - case 200: - return new WebviewResourceResponse.StreamSuccess(response.stream, response.res.headers['etag'], mime); - - case 304: // Not modified - return new WebviewResourceResponse.NotModified(mime); - - default: - return WebviewResourceResponse.Failed; - } - } - try { - const result = await fileService.readFileStream(resourceToLoad, { etag: ifNoneMatch }); + const result = await fileService.readFileStream(resourceToLoad, { etag: options.ifNoneMatch }); return new WebviewResourceResponse.StreamSuccess(result.value, result.etag, mime); } catch (err) { if (err instanceof FileOperationError) { @@ -117,32 +86,16 @@ export async function loadLocalResource( function getResourceToLoad( requestUri: URI, roots: ReadonlyArray, - remoteAuthority: string | undefined, ): URI | undefined { for (const root of roots) { if (containsResource(root, requestUri)) { - return normalizeResourcePath(requestUri, remoteAuthority); + return normalizeResourcePath(requestUri); } } return undefined; } -function normalizeResourcePath(resource: URI, remoteAuthority: string | undefined): URI { - // If the uri was from a remote authority, make we go to the remote to load it - if (remoteAuthority && resource.scheme === Schemas.file) { - return URI.from({ - scheme: Schemas.vscodeRemote, - authority: remoteAuthority, - path: '/vscode-resource', - query: JSON.stringify({ - requestResourcePath: resource.path - }) - }); - } - return resource; -} - function containsResource(root: URI, resource: URI): boolean { let rootPath = root.fsPath + (root.fsPath.endsWith(sep) ? '' : sep); let resourceFsPath = resource.fsPath; @@ -154,3 +107,18 @@ function containsResource(root: URI, resource: URI): boolean { return resourceFsPath.startsWith(rootPath); } + +function normalizeResourcePath(resource: URI): URI { + // Rewrite remote uris to a path that the remote file system can understand + if (resource.scheme === Schemas.vscodeRemote) { + return URI.from({ + scheme: Schemas.vscodeRemote, + authority: resource.authority, + path: '/vscode-resource', + query: JSON.stringify({ + requestResourcePath: resource.path + }) + }); + } + return resource; +} diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 4cc864ed41f..58b84ff93d4 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -13,7 +13,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; @@ -37,7 +36,6 @@ export class IFrameWebview extends BaseWebview implements Web @IMenuService menuService: IMenuService, @INotificationService notificationService: INotificationService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @IRequestService requestService: IRequestService, @ITelemetryService telemetryService: ITelemetryService, @ITunnelService tunnelService: ITunnelService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @@ -48,7 +46,6 @@ export class IFrameWebview extends BaseWebview implements Web logService, telemetryService, environmentService, - requestService, fileService, tunnelService, remoteAuthorityResolverService, @@ -106,7 +103,8 @@ export class IFrameWebview extends BaseWebview implements Web extensionId: extension?.id.value ?? '', // The extensionId and purpose in the URL are used for filtering in js-debug: purpose: options.purpose, serviceWorkerFetchIgnoreSubdomain: options.serviceWorkerFetchIgnoreSubdomain, - ...extraParams + ...extraParams, + 'vscode-resource-origin': this.webviewResourceOrigin, } as const; const queryString = (Object.keys(params) as Array) @@ -124,10 +122,6 @@ export class IFrameWebview extends BaseWebview implements Web return endpoint; } - protected get webviewResourceEndpoint(): string { - return this.webviewContentEndpoint; - } - public mountTo(parent: HTMLElement) { if (this.element) { parent.appendChild(this.element); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 244e5b7c9ed..fb055a5aedb 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -19,7 +19,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { webviewPartitionId } from 'vs/platform/webview/common/webviewManagerService'; import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; @@ -62,7 +61,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme @IMenuService menuService: IMenuService, @INotificationService notificationService: INotificationService, @IFileService fileService: IFileService, - @IRequestService requestService: IRequestService, @ITunnelService tunnelService: ITunnelService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, ) { @@ -74,7 +72,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme environmentService, fileService, menuService, - requestService, tunnelService, remoteAuthorityResolverService }); @@ -162,7 +159,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme // and not the `vscode-file` URI because preload scripts are loaded // via node.js from the main side and only allow `file:` protocol this.element!.preload = FileAccess.asFileUri('./pre/electron-index.js', require).toString(true); - this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser-index.html?platform=electron&id=${this.id}&vscode-resource-origin=${encodeURIComponent(this.webviewResourceEndpoint)}`; + this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser-index.html?platform=electron&id=${this.id}&vscode-resource-origin=${encodeURIComponent(this.webviewResourceOrigin)}`; } protected createElement(options: WebviewOptions) { @@ -195,10 +192,6 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme super.contentOptions = options; } - protected override get webviewResourceEndpoint(): string { - return `https://${this.id}.vscode-webview-test.com`; - } - protected readonly extraContentOptions = {}; public mountTo(parent: HTMLElement) { diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts index 0140d50fa7d..6c66a01bc11 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts @@ -13,7 +13,6 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { IRequestService } from 'vs/platform/request/common/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; @@ -38,7 +37,6 @@ export class ElectronIframeWebview extends IFrameWebview { @IContextMenuService contextMenuService: IContextMenuService, @ITunnelService tunnelService: ITunnelService, @IFileService fileService: IFileService, - @IRequestService requestService: IRequestService, @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IRemoteAuthorityResolverService _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @@ -51,7 +49,7 @@ export class ElectronIframeWebview extends IFrameWebview { ) { super(id, options, contentOptions, extension, webviewThemeDataProvider, contextMenuService, - configurationService, fileService, logService, menuService, notificationService, _remoteAuthorityResolverService, requestService, telemetryService, tunnelService, environmentService); + configurationService, fileService, logService, menuService, notificationService, _remoteAuthorityResolverService, telemetryService, tunnelService, environmentService); this._webviewKeyboardHandler = new WindowIgnoreMenuShortcutsManager(configurationService, mainProcessService, nativeHostService); @@ -66,8 +64,7 @@ export class ElectronIframeWebview extends IFrameWebview { protected override initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) { super.initElement(extension, options, { - platform: 'electron', - 'vscode-resource-origin': this.webviewResourceEndpoint, + platform: 'electron' }); } @@ -79,10 +76,6 @@ export class ElectronIframeWebview extends IFrameWebview { return endpoint; } - protected override get webviewResourceEndpoint(): string { - return `https://${this.id}.vscode-webview-test.com`; - } - protected override async doPostMessage(channel: string, data?: any): Promise { this.element?.contentWindow!.postMessage({ channel, args: data }, '*'); } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts index a31e1564393..bc06cd1a87c 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts @@ -56,7 +56,6 @@ import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/to import { ResourceMap } from 'vs/base/common/map'; import { IFileService } from 'vs/platform/files/common/files'; import { joinPath } from 'vs/base/common/resources'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; import { Schemas } from 'vs/base/common/network'; @@ -132,7 +131,6 @@ export class GettingStartedPage extends EditorPane { @IExtensionService private readonly extensionService: IExtensionService, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IEditorGroupsService private readonly groupsService: IEditorGroupsService, @IContextKeyService contextService: IContextKeyService, @IQuickInputService private quickInputService: IQuickInputService, @@ -494,12 +492,7 @@ export class GettingStartedPage extends EditorPane { if (src.startsWith('https://')) { return `src="${src}"`; } const path = joinPath(base, src); - const transformed = asWebviewUri({ - isExtensionDevelopmentDebug: this.environmentService.isExtensionDevelopment, - webviewResourceRoot: this.environmentService.webviewResourceRoot, - webviewCspSource: this.environmentService.webviewCspSource, - remote: { authority: undefined }, - }, this.webviewID, path).toString(); + const transformed = asWebviewUri(this.webviewID, path, undefined).toString(); return `src="${transformed}"`; }); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 92da6861768..a4046f66004 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -236,17 +236,6 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment return (this.webviewEndpoint).replace('{{commit}}', this.productService.commit || '23a2409675bc1bde94f3532bc7c5826a6e99e4b6'); } - @memoize - get webviewResourceRoot(): string { - return `${this.webviewExternalEndpoint}/vscode-resource/{{resource}}`; - } - - @memoize - get webviewCspSource(): string { - const uri = URI.parse(this.webviewEndpoint.replace('{{uuid}}', '*')); - return `${uri.scheme}://${uri.authority}`; - } - @memoize get telemetryLogResource(): URI { return joinPath(this.options.logsPath, 'telemetry.log'); } get disableTelemetry(): boolean { return false; } diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index 6604c9823ae..f8950d42da8 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -36,8 +36,6 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { readonly extensionEnabledProposedApi?: string[]; readonly webviewExternalEndpoint: string; - readonly webviewResourceRoot: string; - readonly webviewCspSource: string; readonly skipReleaseNotes: boolean; diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index 9fb4fb45d75..948a3aa8935 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -68,21 +68,6 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get webviewExternalEndpoint(): string { return `${Schemas.vscodeWebview}://{{uuid}}`; } - @memoize - get webviewResourceRoot(): string { - // On desktop, this endpoint is only used for the service worker to identify resource loads and - // should never actually be requested. - // - // Required due to https://github.com/electron/electron/issues/28528 - return 'https://{{uuid}}.vscode-webview-test.com/vscode-resource/{{resource}}'; - } - - @memoize - get webviewCspSource(): string { - const uri = URI.parse(this.webviewResourceRoot.replace('{{uuid}}', '*')); - return `${uri.scheme}://${uri.authority}`; - } - @memoize get skipReleaseNotes(): boolean { return !!this.args['skip-release-notes']; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index fee40e289d8..bf44e6b8d70 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -360,8 +360,6 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: this._environmentService.globalStorageHome, workspaceStorageHome: this._environmentService.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { configuration: workspace.configuration || undefined, diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 4b07928a83d..d238526f526 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -235,9 +235,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: remoteInitData.globalStorageHome, - workspaceStorageHome: remoteInitData.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, + workspaceStorageHome: remoteInitData.workspaceStorageHome }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : { configuration: workspace.configuration, diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index a29459cf692..245da60c35b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -476,8 +476,6 @@ export class LocalProcessExtensionHost implements IExtensionHost { extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, globalStorageHome: this._environmentService.globalStorageHome, workspaceStorageHome: this._environmentService.workspaceStorageHome, - webviewResourceRoot: this._environmentService.webviewResourceRoot, - webviewCspSource: this._environmentService.webviewCspSource, }, workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { configuration: withNullAsUndefined(workspace.configuration), diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index 447a1009a0c..f966a1ca426 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -30,12 +30,7 @@ suite('ExtHostWebview', () => { test('Cannot register multiple serializers for the same view type', async () => { const viewType = 'view.type'; - const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { - webviewCspSource: '', - webviewResourceRoot: '', - isExtensionDevelopmentDebug: false, - remote: { authority: undefined }, - }, undefined, new NullLogService(), NullApiDeprecationService); + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { remote: { authority: undefined } }, undefined, new NullLogService(), NullApiDeprecationService); const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); @@ -79,56 +74,68 @@ suite('ExtHostWebview', () => { assert.strictEqual(lastInvokedDeserializer, serializerB); }); - test('asWebviewUri for web endpoint', () => { - const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { - webviewCspSource: '', - webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`, - isExtensionDevelopmentDebug: false, - remote: { - authority: 'remote' - }, - }, undefined, new NullLogService(), NullApiDeprecationService); - - const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); - - const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); - - function stripEndpointUuid(input: string) { - return input.replace(/^https:\/\/[^\.]+?\./, ''); - } + test('asWebviewUri for local file paths', () => { + const webview = createWebview(rpcProtocol, /* remoteAuthority */undefined); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), - 'webview.contoso.com/commit/remote/file//Users/codey/file.html', + 'vscode-webview-test.com/vscode-resource/file//Users/codey/file.html', 'Unix basic' ); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), - 'webview.contoso.com/commit/remote/file//Users/codey/file.html#frag', + 'vscode-webview-test.com/vscode-resource/file//Users/codey/file.html#frag', 'Unix should preserve fragment' ); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), - 'webview.contoso.com/commit/remote/file//Users/codey/f%20ile.html', + 'vscode-webview-test.com/vscode-resource/file//Users/codey/f%20ile.html', 'Unix with encoding' ); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), - 'webview.contoso.com/commit/remote/file/localhost/Users/codey/file.html', + 'vscode-webview-test.com/vscode-resource/file/localhost/Users/codey/file.html', 'Unix should preserve authority' ); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), - 'webview.contoso.com/commit/remote/file//c%3A/codey/file.txt', + 'vscode-webview-test.com/vscode-resource/file//c%3A/codey/file.txt', 'Windows C drive' ); }); + + test('asWebviewUri for remote file paths', () => { + const webview = createWebview(rpcProtocol, /* remoteAuthority */ 'remote'); + + assert.strictEqual( + stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), + 'vscode-webview-test.com/vscode-resource/vscode-remote/remote/Users/codey/file.html', + 'Unix basic' + ); + }); }); +function createWebview(rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined, remoteAuthority: string | undefined) { + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { + remote: { + authority: remoteAuthority + }, + }, undefined, new NullLogService(), NullApiDeprecationService); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as IExtensionDescription, 'type', 'title', 1, {}); + return webview; +} + +function stripEndpointUuid(input: string) { + return input.replace(/^https:\/\/[^\.]+?\./, ''); +} + function createNoopMainThreadWebviews() { return new class extends mock() { From e65a227c8297cc690bdc78337b1d0786baad83d4 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 17:55:22 -0700 Subject: [PATCH 181/270] Test using readonly[] syntax in vscode.d.ts --- src/vs/vscode.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 53a4a1c5361..118837f3aad 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2355,7 +2355,7 @@ declare module 'vscode' { * list of kinds may either be generic, such as `[CodeActionKind.Refactor]`, or list out every kind provided, * such as `[CodeActionKind.Refactor.Extract.append('function'), CodeActionKind.Refactor.Extract.append('constant'), ...]`. */ - readonly providedCodeActionKinds?: ReadonlyArray; + readonly providedCodeActionKinds?: readonly CodeActionKind[]; /** * Static documentation for a class of code actions. From 35eafb787328bfc822d9df1e51d43b09b8b8face Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 18:20:16 -0700 Subject: [PATCH 182/270] Add sort imports command Fixes #122593 Sort imports just sorts imports, vs organize imports which also removes unused imports --- .../src/languageFeatures/organizeImports.ts | 79 +++++++++++++------ 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts index 3b8111b7c1f..10a399bad5b 100644 --- a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts +++ b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts @@ -29,7 +29,7 @@ class OrganizeImportsCommand implements Command { private readonly telemetryReporter: TelemetryReporter, ) { } - public async execute(file: string): Promise { + public async execute(file: string, sortOnly = false): Promise { /* __GDPR__ "organizeImports.execute" : { "${include}": [ @@ -45,7 +45,8 @@ class OrganizeImportsCommand implements Command { args: { file } - } + }, + skipDestructiveCodeActions: sortOnly, }; const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, nulToken)); if (response.type !== 'response' || !response.body) { @@ -57,23 +58,42 @@ class OrganizeImportsCommand implements Command { } } -export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvider { - public static readonly minVersion = API.v280; +class ImportsCodeActionProvider implements vscode.CodeActionProvider { + + static register( + client: ITypeScriptServiceClient, + minVersion: API, + kind: vscode.CodeActionKind, + title: string, + sortOnly: boolean, + commandManager: CommandManager, + fileConfigurationManager: FileConfigurationManager, + telemetryReporter: TelemetryReporter, + selector: DocumentSelector + ): vscode.Disposable { + return conditionalRegistration([ + requireMinVersion(client, minVersion), + requireSomeCapability(client, ClientCapability.Semantic), + ], () => { + const provider = new ImportsCodeActionProvider(client, kind, title, sortOnly, commandManager, fileConfigurationManager, telemetryReporter); + return vscode.languages.registerCodeActionsProvider(selector.semantic, provider, { + providedCodeActionKinds: [kind] + }); + }); + } public constructor( private readonly client: ITypeScriptServiceClient, + private readonly kind: vscode.CodeActionKind, + private readonly title: string, + private readonly sortOnly: boolean, commandManager: CommandManager, private readonly fileConfigManager: FileConfigurationManager, telemetryReporter: TelemetryReporter, - ) { commandManager.register(new OrganizeImportsCommand(client, telemetryReporter)); } - public readonly metadata: vscode.CodeActionProviderMetadata = { - providedCodeActionKinds: [vscode.CodeActionKind.SourceOrganizeImports] - }; - public provideCodeActions( document: vscode.TextDocument, _range: vscode.Range, @@ -85,16 +105,14 @@ export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvi return []; } - if (!context.only || !context.only.contains(vscode.CodeActionKind.SourceOrganizeImports)) { + if (!context.only || !context.only.contains(this.kind)) { return []; } this.fileConfigManager.ensureConfigurationForDocument(document, token); - const action = new vscode.CodeAction( - localize('organizeImportsAction.title', "Organize Imports"), - vscode.CodeActionKind.SourceOrganizeImports); - action.command = { title: '', command: OrganizeImportsCommand.Id, arguments: [file] }; + const action = new vscode.CodeAction(this.title, this.kind); + action.command = { title: '', command: OrganizeImportsCommand.Id, arguments: [file, this.sortOnly] }; return [action]; } } @@ -106,13 +124,28 @@ export function register( fileConfigurationManager: FileConfigurationManager, telemetryReporter: TelemetryReporter, ) { - return conditionalRegistration([ - requireMinVersion(client, OrganizeImportsCodeActionProvider.minVersion), - requireSomeCapability(client, ClientCapability.Semantic), - ], () => { - const organizeImportsProvider = new OrganizeImportsCodeActionProvider(client, commandManager, fileConfigurationManager, telemetryReporter); - return vscode.languages.registerCodeActionsProvider(selector.semantic, - organizeImportsProvider, - organizeImportsProvider.metadata); - }); + return vscode.Disposable.from( + ImportsCodeActionProvider.register( + client, + API.v280, + vscode.CodeActionKind.SourceOrganizeImports, + localize('organizeImportsAction.title', "Organize Imports"), + false, + commandManager, + fileConfigurationManager, + telemetryReporter, + selector + ), + ImportsCodeActionProvider.register( + client, + API.v430, + vscode.CodeActionKind.Source.append('sortImports'), + localize('sortImportsAction.title', "Sort Imports"), + true, + commandManager, + fileConfigurationManager, + telemetryReporter, + selector + ), + ); } From 34180ac9bef8c781c25952e26b29d8daa43bb451 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 18:23:51 -0700 Subject: [PATCH 183/270] Remove code allowing multiple ids for a command This doesn't appear to be used anywhere --- .../src/commands/commandManager.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/extensions/typescript-language-features/src/commands/commandManager.ts b/extensions/typescript-language-features/src/commands/commandManager.ts index 7bf1d7e33aa..b7b58bf7ede 100644 --- a/extensions/typescript-language-features/src/commands/commandManager.ts +++ b/extensions/typescript-language-features/src/commands/commandManager.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; export interface Command { - readonly id: string | string[]; + readonly id: string; execute(...args: any[]): void; } @@ -22,17 +22,9 @@ export class CommandManager { } public register(command: T): T { - for (const id of Array.isArray(command.id) ? command.id : [command.id]) { - this.registerCommand(id, command.execute, command); + if (!this.commands.has(command.id)) { + this.commands.set(command.id, vscode.commands.registerCommand(command.id, command.execute, command)); } return command; } - - private registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { - if (this.commands.has(id)) { - return; - } - - this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); - } -} \ No newline at end of file +} From 29c61570a5b9a669f777bb28b5acd5c37d99edbe Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 19 May 2021 18:57:47 -0700 Subject: [PATCH 184/270] Revert "Closes #122433" This reverts commit 134d9b187bbac1ef64be25ca805d210208a2c0aa. --- .../editor/browser/editorOverrideService.ts | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 45c897382db..990a4acf6d9 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -40,13 +40,9 @@ type ContributionPoints = Array; export class EditorOverrideService extends Disposable implements IEditorOverrideService { readonly _serviceBrand: undefined; - // Constants private static readonly configureDefaultID = 'promptOpenWith.configureDefault'; - private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; - private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; - - // Data Stores private _contributionPoints: Map = new Map(); + private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; private cache: Set | undefined; constructor( @@ -133,8 +129,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } const input = await this.doOverrideEditorInput(editor, options, group, selectedContribution); if (conflictingDefault && input) { - // Show the conflicting default dialog - await this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); + // Wait one second to give the user ample time to see the current editor then ask them to configure a default + this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); } // Add the group as we might've changed it with the quickpick @@ -371,23 +367,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } private async doHandleConflictingDefaults(editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { - type StoredChoice = { - [key: string]: string[]; + const makeCurrentEditorDefault = () => { + const viewType = currentEditor.viewType; + if (viewType) { + this.updateUserAssociations(`*${extname(currentEditor.resource!)}`, viewType); + } }; - const contributionPoints = this.findMatchingContributions(currentEditor.resource!); - const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorOverrideService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); - const globForResource = `*${extname(currentEditor.resource!)}`; - // Writes to the storage service that a choice has been made for the currently installed editors - const writeCurrentEditorsToStorage = () => { - storedChoices[globForResource] = []; - contributionPoints.forEach(contrib => storedChoices[globForResource].push(contrib.editorInfo.id)); - this.storageService.store(EditorOverrideService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); - }; - - // If the user has already made a choice for this editor we don't want to ask them again - if (storedChoices[globForResource]?.find(editorID => editorID === currentEditor.viewType)) { - return; - } const handle = this.notificationService.prompt(Severity.Warning, localize('editorOverride.conflictingDefaults', 'There are multiple default editors available for the resource.'), @@ -415,12 +400,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }, { label: localize('editorOverride.keepDefault', 'Keep {0}', editorName), - run: writeCurrentEditorsToStorage + run: makeCurrentEditorDefault } ]); // If the user pressed X we assume they want to keep the current editor as default const onCloseListener = handle.onDidClose(() => { - writeCurrentEditorsToStorage(); + makeCurrentEditorDefault(); onCloseListener.dispose(); }); } From 0c996a39b651ab2f46296435299db662fd10453c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 19 May 2021 21:31:06 -0700 Subject: [PATCH 185/270] Implement consolidated output toolbar --- src/vs/platform/actions/common/actions.ts | 1 + .../notebook/browser/contrib/coreActions.ts | 20 +-- .../notebook/browser/media/notebook.css | 4 +- .../notebook/browser/notebookBrowser.ts | 1 + .../notebook/browser/notebookEditorWidget.ts | 7 +- .../notebookEditorWidgetContextKeys.ts | 15 ++- .../browser/view/renderers/cellOutput.ts | 118 +++++++++++------- .../contrib/notebook/common/notebookCommon.ts | 1 + .../notebook/common/notebookOptions.ts | 14 ++- 9 files changed, 115 insertions(+), 66 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 1a7d4f12fca..179c0797983 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -153,6 +153,7 @@ export class MenuId { static readonly NotebookDiffCellInputTitle = new MenuId('NotebookDiffCellInputTitle'); static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle'); static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle'); + static readonly NotebookOutputToolbar = new MenuId('NotebookOutputToolbar'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly TimelineItemContext = new MenuId('TimelineItemContext'); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 2a79d2bf867..4cad4eb65fe 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -18,7 +18,7 @@ import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/context import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_INPUT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_INPUT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditType, CellKind, ICellEditOperation, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellExecutionState, TransientCellMetadata, TransientDocumentMetadata, SelectionStateType, ICellReplaceEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, isICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -1308,12 +1308,18 @@ registerAction2(class ClearCellOutputsAction extends NotebookCellAction { super({ id: CLEAR_CELL_OUTPUTS_COMMAND_ID, title: localize('clearCellOutputs', 'Clear Cell Outputs'), - menu: { - id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), executeNotebookCondition, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), - order: CellToolbarOrder.ClearCellOutput, - group: CELL_TITLE_OUTPUT_GROUP_ID - }, + menu: [ + { + id: MenuId.NotebookCellTitle, + when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), executeNotebookCondition, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON.toNegated()), + order: CellToolbarOrder.ClearCellOutput, + group: CELL_TITLE_OUTPUT_GROUP_ID + }, + { + id: MenuId.NotebookOutputToolbar, + when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE) + }, + ], keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), primary: KeyMod.Alt | KeyCode.Delete, diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index cc070968973..3534ad26e6a 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -254,11 +254,11 @@ color: red; /*TODO@rebornix theme color*/ } -.monaco-workbench .notebookOverlay .cell-drag-image .output .multi-mimetype-output { +.monaco-workbench .notebookOverlay .cell-drag-image .output .cell-output-toolbar { display: none; } -.monaco-workbench .notebookOverlay .output .multi-mimetype-output { +.monaco-workbench .notebookOverlay .output .cell-output-toolbar { position: absolute; top: 4px; left: -30px; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index b4dbbc4020b..90bdc027e00 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -49,6 +49,7 @@ export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCe export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_HAS_RUNNING_CELL = new RawContextKey('notebookHasRunningCell', false); +export const NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON = new RawContextKey('notebookUseConsolidatedOutputButton', false); // Cell keys export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookViewType', undefined); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 22840d525ad..3cf28488285 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2657,17 +2657,14 @@ export const cellEditorBackground = registerColor('notebook.cellEditorBackground registerThemingParticipant((theme, collector) => { const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.notebookOverlay .output a, - .notebookOverlay .cell.markdown a, + collector.addRule(`.notebookOverlay .cell.markdown a, .notebookOverlay .output-show-more-container a { color: ${link};} `); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { - collector.addRule(`.notebookOverlay .output a:hover, - .notebookOverlay .cell .output a:active, - .notebookOverlay .output-show-more-container a:active + collector.addRule(`.notebookOverlay .output-show-more-container a:active { color: ${activeLink}; }`); } const shortcut = theme.getColor(textPreformatForeground); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts index 8df4ae49e95..d51c5574af7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys.ts @@ -5,7 +5,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ICellViewModel, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL_COUNT, INotebookEditor, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditor, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; @@ -16,6 +16,7 @@ export class NotebookEditorContextKeys { private readonly _notebookKernelSelected: IContextKey; private readonly _interruptibleKernel: IContextKey; private readonly _someCellRunning: IContextKey; + private readonly _useConsolidatedOutputButton: IContextKey; private _viewType!: IContextKey; private readonly _disposables = new DisposableStore(); @@ -31,12 +32,18 @@ export class NotebookEditorContextKeys { this._notebookKernelSelected = NOTEBOOK_KERNEL_SELECTED.bindTo(contextKeyService); this._interruptibleKernel = NOTEBOOK_INTERRUPTIBLE_KERNEL.bindTo(contextKeyService); this._someCellRunning = NOTEBOOK_HAS_RUNNING_CELL.bindTo(contextKeyService); + this._useConsolidatedOutputButton = NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON.bindTo(contextKeyService); this._viewType = NOTEBOOK_VIEW_TYPE.bindTo(contextKeyService); + this._handleDidChangeModel(); + this._updateForNotebookOptions(); + this._disposables.add(_editor.onDidChangeModel(this._handleDidChangeModel, this)); this._disposables.add(_notebookKernelService.onDidAddKernel(this._updateKernelContext, this)); this._disposables.add(_notebookKernelService.onDidChangeNotebookKernelBinding(this._updateKernelContext, this)); - this._handleDidChangeModel(); + this._disposables.add(_editor.notebookOptions.onDidChangeOptions(() => { + this._updateForNotebookOptions(); + })); } dispose(): void { @@ -104,4 +111,8 @@ export class NotebookEditorContextKeys { this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); this._notebookKernelSelected.set(Boolean(selected)); } + + private _updateForNotebookOptions(): void { + this._useConsolidatedOutputButton.set(this._editor.notebookOptions.getLayoutConfiguration().consolidatedOutputButton); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts index 421cbba176b..da239a7d247 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts @@ -3,24 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Schemas } from 'vs/base/common/network'; import * as DOM from 'vs/base/browser/dom'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { Action, IAction } from 'vs/base/common/actions'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import * as nls from 'vs/nls'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { CodeCellRenderTemplate, ICellOutputViewModel, IInsetRenderOutput, INotebookEditor, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { BUILTIN_RENDERER_ID, CellUri, NotebookCellOutputsSplice, IOrderedMimeType, INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { BUILTIN_RENDERER_ID, CellUri, INotebookKernel, IOrderedMimeType, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; const OUTPUT_COUNT_LIMIT = 500; @@ -33,7 +39,9 @@ interface IRenderResult { } export class CellOutputElement extends Disposable { - readonly localDisposableStore = new DisposableStore(); + private readonly _renderDisposableStore = this._register(new DisposableStore()); + private readonly _actionsDisposable = this._register(new MutableDisposable()); + domNode!: HTMLElement; renderResult?: IRenderOutput; @@ -47,21 +55,27 @@ export class CellOutputElement extends Disposable { } } + private readonly contextKeyService: IContextKeyService; + constructor( private notebookEditor: INotebookEditor, - private notebookService: INotebookService, - private quickInputService: IQuickInputService, private viewCell: CodeCellViewModel, private outputContainer: HTMLElement, - readonly output: ICellOutputViewModel + readonly output: ICellOutputViewModel, + @INotebookService private readonly notebookService: INotebookService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IContextKeyService parentContextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, ) { super(); + this.contextKeyService = this._register(parentContextKeyService.createScoped(this.outputContainer)); + this._register(this.output.model.onDidChangeData(() => { this.updateOutputRendering(); })); - - this._register(this.localDisposableStore); } detach() { @@ -86,7 +100,7 @@ export class CellOutputElement extends Disposable { // user chooses another mimetype const index = this.viewCell.outputsViewModels.indexOf(this.output); const nextElement = this.domNode.nextElementSibling; - this.localDisposableStore.clear(); + this._renderDisposableStore.clear(); const element = this.domNode; if (element) { element.parentElement?.removeChild(element); @@ -118,9 +132,7 @@ export class CellOutputElement extends Disposable { this.domNode = this.useDedicatedDOM ? DOM.$('.output-inner-container') : this.outputContainer.lastChild as HTMLElement; this.domNode.setAttribute('output-mime-type', pickedMimeTypeRenderer.mimeType); - if (mimeTypes.filter(mimeType => mimeType.isTrusted).length > 1) { - this.attachMimetypeSwitcher(this.domNode, notebookTextModel, this.notebookEditor.activeKernel, mimeTypes); - } + this.attachToolbar(this.domNode, notebookTextModel, this.notebookEditor.activeKernel, index, mimeTypes); const notebookUri = CellUri.parse(this.viewCell.uri)?.notebook; if (!notebookUri) { @@ -207,7 +219,7 @@ export class CellOutputElement extends Disposable { }); elementSizeObserver.startObserving(); - this.localDisposableStore.add(elementSizeObserver); + this._renderDisposableStore.add(elementSizeObserver); } private previousDivSupportAppend(mimeType: string) { @@ -220,29 +232,42 @@ export class CellOutputElement extends Disposable { return false; } - private async attachMimetypeSwitcher(outputItemDiv: HTMLElement, notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, mimeTypes: readonly IOrderedMimeType[]) { + private async attachToolbar(outputItemDiv: HTMLElement, notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, index: number, mimeTypes: readonly IOrderedMimeType[]) { + const hasMultipleMimeTypes = mimeTypes.filter(mimeType => mimeType.isTrusted).length <= 1; + if (index > 0 && hasMultipleMimeTypes) { + return; + } + + const useConsolidatedButton = this.notebookEditor.notebookOptions.getLayoutConfiguration().consolidatedOutputButton; + outputItemDiv.style.position = 'relative'; - const mimeTypePicker = DOM.$('.multi-mimetype-output'); - mimeTypePicker.classList.add(...ThemeIcon.asClassNameArray(mimetypeIcon)); - mimeTypePicker.tabIndex = 0; - mimeTypePicker.title = nls.localize('mimeTypePicker', "Choose a different output mimetype, available mimetypes: {0}", mimeTypes.map(mimeType => mimeType.mimeType).join(', ')); + const mimeTypePicker = DOM.$('.cell-output-toolbar'); + outputItemDiv.appendChild(mimeTypePicker); - this.localDisposableStore.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => { - if (e.leftButton) { - e.preventDefault(); - e.stopPropagation(); - await this.pickActiveMimeTypeRenderer(notebookTextModel, kernel, this.output); - } + + const toolbar = this._renderDisposableStore.add(new ToolBar(mimeTypePicker, this.contextMenuService, { + getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), + renderDropdownAsChildElement: true })); - this.localDisposableStore.add((DOM.addDisposableListener(mimeTypePicker, DOM.EventType.KEY_DOWN, async e => { - const event = new StandardKeyboardEvent(e); - if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) { - e.preventDefault(); - e.stopPropagation(); - await this.pickActiveMimeTypeRenderer(notebookTextModel, kernel, this.output); - } - }))); + // TODO: This could probably be a real registered action, but it has to talk to this output element + const pickAction = new Action('notebook.output.pickMimetype', nls.localize('pickMimeType', "Choose a different output mimetype"), ThemeIcon.asClassName(mimetypeIcon), undefined, + async _context => this.pickActiveMimeTypeRenderer(notebookTextModel, kernel, this.output)); + if (index === 0 && useConsolidatedButton) { + const menu = this.menuService.createMenu(MenuId.NotebookOutputToolbar, this.contextKeyService); + const updateMenuToolbar = () => { + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + this._actionsDisposable.value = createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result, () => false); + toolbar.setActions([], [pickAction, ...secondary]); + }; + updateMenuToolbar(); + this._renderDisposableStore.add(menu.onDidChange(updateMenuToolbar)); + } else { + toolbar.setActions([pickAction]); + } } private async pickActiveMimeTypeRenderer(notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, viewModel: ICellOutputViewModel) { @@ -284,7 +309,7 @@ export class CellOutputElement extends Disposable { // user chooses another mimetype const index = this.viewCell.outputsViewModels.indexOf(viewModel); const nextElement = this.domNode.nextElementSibling; - this.localDisposableStore.clear(); + this._renderDisposableStore.clear(); const element = this.domNode; if (element) { element.parentElement?.removeChild(element); @@ -356,9 +381,8 @@ export class CellOutputContainer extends Disposable { private notebookEditor: INotebookEditor, private viewCell: CodeCellViewModel, private templateData: CodeCellRenderTemplate, - @INotebookService private readonly notebookService: INotebookService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IOpenerService private readonly openerService: IOpenerService + @IOpenerService private readonly openerService: IOpenerService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -546,7 +570,7 @@ export class CellOutputContainer extends Disposable { private _renderOutput(currOutput: ICellOutputViewModel, index: number, beforeElement?: HTMLElement) { if (!this.outputEntries.has(currOutput)) { - this.outputEntries.set(currOutput, new CellOutputElement(this.notebookEditor, this.notebookService, this.quickInputService, this.viewCell, this.templateData.outputContainer, currOutput)); + this.outputEntries.set(currOutput, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this.templateData.outputContainer, currOutput)); } return this.outputEntries.get(currOutput)!.render(index, beforeElement); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 8eda48a57b3..52c6227d7e4 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -904,6 +904,7 @@ export const ExperimentalFocusIndicator = 'notebook.experimental.cellFocusIndica export const ExperimentalInsertToolbarPosition = 'notebook.experimental.insertToolbarPosition'; export const ExperimentalGlobalToolbar = 'notebook.experimental.globalToolbar'; export const ExperimentalUndoRedoPerCell = 'notebook.experimental.undoRedoPerCell'; +export const ExperimentalConsolidatedOutputButton = 'notebook.experimental.consolidatedOutputButton'; export const enum CellStatusbarAlignment { Left = 1, diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index 2dde37f10d1..cd20b2d700b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -6,7 +6,7 @@ import { Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalFocusIndicator, ExperimentalGlobalToolbar, ExperimentalInsertToolbarPosition, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalConsolidatedOutputButton, ExperimentalFocusIndicator, ExperimentalGlobalToolbar, ExperimentalInsertToolbarPosition, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; const SCROLLABLE_ELEMENT_PADDING_TOP = 18; @@ -50,6 +50,7 @@ export interface NotebookLayoutConfiguration { focusIndicator: 'border' | 'gutter'; insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'; globalToolbar: boolean; + consolidatedOutputButton: boolean; } interface NotebookOptionsChangeEvent { @@ -85,9 +86,10 @@ export class NotebookOptions { readonly onDidChangeOptions = this._onDidChangeOptions.event; private _disposables: IDisposable[]; - constructor(readonly configurationService: IConfigurationService) { + constructor(private readonly configurationService: IConfigurationService) { const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); const globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; + const consolidatedOutputButton = this.configurationService.getValue(ExperimentalConsolidatedOutputButton) ?? true; const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); const cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); const compactView = this.configurationService.getValue(ExperimentalCompactView); @@ -113,6 +115,7 @@ export class NotebookOptions { collapsedIndicatorHeight: 24, showCellStatusBar, globalToolbar, + consolidatedOutputButton, cellToolbarLocation, cellToolbarInteraction, compactView, @@ -128,8 +131,9 @@ export class NotebookOptions { let focusIndicator = e.affectsConfiguration(ExperimentalFocusIndicator); let insertToolbarPosition = e.affectsConfiguration(ExperimentalInsertToolbarPosition); let globalToolbar = e.affectsConfiguration(ExperimentalGlobalToolbar); + let consolidatedOutputButton = e.affectsConfiguration(ExperimentalConsolidatedOutputButton); - if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator && !insertToolbarPosition && !globalToolbar) { + if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator && !insertToolbarPosition && !globalToolbar && !consolidatedOutputButton) { return; } @@ -170,6 +174,10 @@ export class NotebookOptions { configuration.globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; } + if (consolidatedOutputButton) { + configuration.consolidatedOutputButton = this.configurationService.getValue(ExperimentalConsolidatedOutputButton) ?? true; + } + this._layoutConfiguration = configuration; // trigger event From a6f89f58a9e81ee9ae4392e8176f24a5d4d3cde0 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 20 May 2021 08:34:18 +0200 Subject: [PATCH 186/270] Workspace trust - empty workspace (#123811) Co-authored-by: Logan Ramos --- .../workspace/common/workspaceTrust.ts | 2 + .../browser/textFileEditorTracker.test.ts | 3 + .../browser/workspace.contribution.ts | 45 ++++- .../workspace/browser/workspaceTrustEditor.ts | 62 +++++-- src/vs/workbench/electron-sandbox/window.ts | 7 +- .../services/editor/browser/editorService.ts | 31 +++- .../editor/test/browser/editorService.test.ts | 3 + .../browser/workingCopyBackupTracker.test.ts | 4 + .../browser/workingCopyEditorService.test.ts | 3 + .../workspaces/common/workspaceTrust.ts | 155 ++++++++++++------ .../test/common/testWorkspaceTrustService.ts | 10 +- 11 files changed, 244 insertions(+), 81 deletions(-) diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 06554ed2cb5..6a84d09b027 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -41,6 +41,8 @@ export interface IWorkspaceTrustManagementService { onDidChangeTrust: WorkspaceTrustChangeEvent; onDidChangeTrustedFolders: Event; + get acceptsNonWorkspaceFiles(): boolean; + set acceptsNonWorkspaceFiles(value: boolean); addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable; isWorkpaceTrusted(): boolean; canSetParentFolderTrust(): boolean; diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index c9082eeec1b..bd45cd731f0 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -25,6 +25,8 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; suite('Files - TextFileEditorTracker', () => { @@ -57,6 +59,7 @@ suite('Files - TextFileEditorTracker', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 0dbac3b6736..5ceedd2ab87 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -25,10 +25,10 @@ import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarA import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { shieldIcon, WorkspaceTrustEditor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustEditor'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; -import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; -import { EditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { EditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { isWeb } from 'vs/base/common/platform'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; @@ -67,6 +67,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben constructor( @IDialogService private readonly dialogService: IDialogService, @ICommandService private readonly commandService: ICommandService, + @IEditorService private readonly editorService: IEditorService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -84,6 +85,10 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben this.registerListeners(); this.createStatusbarEntry(); + // Set empty workspace trust state + this.setEmptyWorkspaceTrustState(); + + // Show modal dialog if (this.hostService.hasFocus) { this.showModalOnStart(); } else { @@ -165,6 +170,12 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben return; } + // Don't show modal prompt for empty workspaces by default + if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY) { + this.updateWorkbenchIndicators(false); + return; + } + if (this.startupPromptSetting === 'never') { this.updateWorkbenchIndicators(false); return; @@ -306,6 +317,25 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben }; } + private setEmptyWorkspaceTrustState(): void { + if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY) { + return; + } + + // Open files + const openFiles = this.editorService.editors.map(editor => EditorResourceAccessor.getCanonicalUri(editor, { filterByScheme: Schemas.file })).filter(uri => !!uri); + + if (openFiles.length) { + // If all open files are trusted, transition to a trusted workspace + if (openFiles.map(uri => this.workspaceTrustManagementService.getUriTrustInfo(uri!).trusted).every(trusted => trusted)) { + this.workspaceTrustManagementService.setWorkspaceTrust(true); + } + } else { + // No open files, use the setting to set workspace trust state + this.workspaceTrustManagementService.setWorkspaceTrust(this.configurationService.getValue(WORKSPACE_TRUST_EMPTY_WINDOW) ?? false); + } + } + private updateStatusbarEntry(trusted: boolean): void { this.statusbarEntryAccessor.value?.update(this.getStatusbarEntry(trusted)); this.updateStatusbarEntryVisibility(trusted); @@ -511,7 +541,7 @@ Registry.as(ConfigurationExtensions.Configuration) default: verifyMicrosoftInternalDomain(product.msftInternalDomains || []), included: !isWeb, description: localize('workspace.trust.description', "Controls whether or not workspace trust is enabled within VS Code."), - scope: ConfigurationScope.APPLICATION + scope: ConfigurationScope.APPLICATION, }, [WORKSPACE_TRUST_STARTUP_PROMPT]: { type: 'string', @@ -525,6 +555,13 @@ Registry.as(ConfigurationExtensions.Configuration) localize('workspace.trust.startupPrompt.once', "Ask for trust the first time an untrusted workspace is opened."), localize('workspace.trust.startupPrompt.never', "Do not ask for trust when an untrusted workspace is opened."), ] + }, + [WORKSPACE_TRUST_EMPTY_WINDOW]: { + type: 'boolean', + default: false, + included: !isWeb, + description: localize('workspace.trust.emptyWindow.description', "Controls whether or not the empty window is trusted by default within VS Code."), + scope: ConfigurationScope.APPLICATION } } }); diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 82016b8c8d1..850a2727adb 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -37,9 +37,9 @@ import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { buttonBackground, buttonSecondaryBackground, editorErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { attachButtonStyler, attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; @@ -604,17 +604,19 @@ export class WorkspaceTrustEditor extends EditorPane { return 'workspace-trust-header workspace-trust-untrusted'; } - private useWorkspaceLanguage(): boolean { - return !isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())); - } - private getHeaderTitleText(trusted: boolean): string { - if (trusted) { - return this.useWorkspaceLanguage() ? localize('trustedHeaderWorkspace', "You trust this workspace") : localize('trustedHeaderFolder', "You trust this folder"); + switch (this.workspaceService.getWorkbenchState()) { + case WorkbenchState.EMPTY: + return localize('trustedHeaderWindow', "You trust this window"); + case WorkbenchState.FOLDER: + return localize('trustedHeaderFolder', "You trust this folder"); + case WorkbenchState.WORKSPACE: + return localize('trustedHeaderWorkspace', "You trust this workspace"); + } } - return this.useWorkspaceLanguage() ? localize('untrustedHeaderWorkspace', "You are in restricted mode") : localize('untrustedHeaderFolder', "You are in Restricted Mode"); + return localize('untrustedHeader', "You are in Restricted Mode"); } private getHeaderDescriptionText(trusted: boolean): string { @@ -629,6 +631,34 @@ export class WorkspaceTrustEditor extends EditorPane { return shieldIcon.classNamesArray; } + private getFeaturesHeaderText(trusted: boolean): [string, string] { + let title: string = ''; + let subTitle: string = ''; + + switch (this.workspaceService.getWorkbenchState()) { + case WorkbenchState.EMPTY: { + title = trusted ? localize('trustedWindow', "In a trusted window") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedWindowSubtitle', "You trust the authors of the files in the current window. All features are enabled:") : + localize('untrustedWindowSubtitle', "You do not trust the authors of the files in the current window. The following features are disabled:"); + break; + } + case WorkbenchState.FOLDER: { + title = trusted ? localize('trustedFolder', "In a trusted folder") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedFolderSubtitle', "You trust the authors of the files in the current folder. All features are enabled:") : + localize('untrustedFolderSubtitle', "You do not trust the authors of the files in the current folder. The following features are disabled:"); + break; + } + case WorkbenchState.WORKSPACE: { + title = trusted ? localize('trustedWorkspace', "In a trusted workspace") : localize('untrustedWorkspace', "In Restricted Mode"); + subTitle = trusted ? localize('trustedWorkspaceSubtitle', "You trust the authors of the files in the current workspace. All features are enabled:") : + localize('untrustedWorkspaceSubtitle', "You do not trust the authors of the files in the current workspace. The following features are disabled:"); + break; + } + } + + return [title, subTitle]; + } + private rendering = false; private rerenderDisposables: DisposableStore = this._register(new DisposableStore()); @debounce(100) @@ -750,9 +780,8 @@ export class WorkspaceTrustEditor extends EditorPane { private renderAffectedFeatures(numSettings: number, numExtensions: number): void { clearNode(this.affectedFeaturesContainer); const trustedContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-limitations.trusted')); - this.renderLimitationsHeaderElement(trustedContainer, - this.useWorkspaceLanguage() ? localize('trustedWorkspace', "In a trusted workspace") : localize('trustedFolder', "In a Trusted Folder"), - this.useWorkspaceLanguage() ? localize('trustedWorkspaceSubtitle', "You trust the authors of the files in the current workspace. All features are enabled:") : localize('trustedFolderSubtitle', "You trust the authors of the files in the current folder. All features are enabled:")); + const [trustedTitle, trustedSubTitle] = this.getFeaturesHeaderText(true); + this.renderLimitationsHeaderElement(trustedContainer, trustedTitle, trustedSubTitle); this.renderLimitationsListElement(trustedContainer, [ localize('trustedTasks', "Tasks are allowed to run"), localize('trustedDebugging', "Debugging is enabled"), @@ -761,9 +790,8 @@ export class WorkspaceTrustEditor extends EditorPane { ], checkListIcon.classNamesArray); const untrustedContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-limitations.untrusted')); - this.renderLimitationsHeaderElement(untrustedContainer, - localize('untrustedWorkspace', "In Restricted Mode"), - this.useWorkspaceLanguage() ? localize('untrustedWorkspaceSubtitle', "You do not trust the authors of the files in the current workspace. The following features are disabled:") : localize('untrustedFolderSubtitle', "You do not trust the authors of the files in the current folder. The following features are disabled:")); + const [untrustedTitle, untrustedSubTitle] = this.getFeaturesHeaderText(false); + this.renderLimitationsHeaderElement(untrustedContainer, untrustedTitle, untrustedSubTitle); this.renderLimitationsListElement(untrustedContainer, [ localize('untrustedTasks', "Tasks are disabled"), @@ -855,8 +883,12 @@ export class WorkspaceTrustEditor extends EditorPane { } private addTrustedTextToElement(parent: HTMLElement): void { + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return; + } + const textElement = append(parent, $('.workspace-trust-untrusted-description')); - textElement.innerText = this.useWorkspaceLanguage() ? localize('untrustedWorkspaceReason', "This workspace is trusted via the bolded entries in the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via the bolded entries in the the trusted folders below."); + textElement.innerText = this.workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE ? localize('untrustedWorkspaceReason', "This workspace is trusted via the bolded entries in the trusted folders below.") : localize('untrustedFolderReason', "This folder is trusted via the bolded entries in the the trusted folders below."); } private renderLimitationsHeaderElement(parent: HTMLElement, headerText: string, subtitleText: string): void { diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 23c3f609359..7754e9cdb0e 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -661,12 +661,7 @@ export class NativeWindow extends Disposable { return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } }); } - // For one file, just put it into the current active editor - if (resources.length === 1) { - return this.editorService.openEditor(resources[0]); - } - - // Otherwise open all + // Open resource(s) return this.editorService.openEditors(resources); } } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 8368e50d9f9..41957dba74a 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -37,6 +37,9 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ILogService } from 'vs/platform/log/common/log'; import { ContributedEditorPriority, DEFAULT_EDITOR_ASSOCIATION, IEditorOverrideService } from 'vs/workbench/services/editor/common/editorOverrideService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IFileToOpen } from 'vs/platform/windows/common/windows'; type CachedEditorInput = TextResourceEditorInput | IFileEditorInput | UntitledTextEditorInput; type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; @@ -76,7 +79,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService, @IEditorOverrideService private readonly editorOverrideService: IEditorOverrideService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IHostService private readonly hostService: IHostService, ) { super(); @@ -787,12 +792,28 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#endregion + //#region Workspace Trust + /** + * Given a set of resources returns a boolean to indicate whether or not to proceed with opening + */ + private async handleWorkspaceTrust(resources: Array): Promise { + const filteredResources = coalesce(resources); + const trustResult = await this.workspaceTrustRequestService.requestOpenUris(filteredResources); + if (trustResult === WorkspaceTrustUriResponse.OpenInNewWindow) { + const filesToOpen: IFileToOpen[] = []; + filteredResources.forEach(resource => filesToOpen.push({ fileUri: resource })); + await this.hostService.openWindow(filesToOpen, { forceNewWindow: true }); + return false; + } + return trustResult === WorkspaceTrustUriResponse.Open; + } + //#endregion + //#region openEditors() openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise; async openEditors(editors: Array, group?: OpenInEditorGroup): Promise { - // Convert to typed editors and options const typedEditors: IEditorInputWithOptions[] = editors.map(editor => { if (isEditorInputWithOptions(editor)) { @@ -805,6 +826,12 @@ export class EditorService extends Disposable implements EditorServiceImpl { }; }); + const editorResources = typedEditors.map(e => e.editor.resource); + if (!(await this.handleWorkspaceTrust(editorResources))) { + typedEditors.forEach(editor => editor.editor.dispose()); + return []; + } + // Find target groups to open const mapGroupToEditorsCandidates = new Map(); if (group === SIDE_GROUP) { diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index a5ff884573c..0b1b0d3cf5d 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -31,6 +31,8 @@ import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServic import { isLinux } from 'vs/base/common/platform'; import { MockScopableContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ContributedEditorPriority } from 'vs/workbench/services/editor/common/editorOverrideService'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; suite('EditorService', () => { @@ -61,6 +63,7 @@ suite('EditorService', () => { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts index dc5ff60e956..29149d216d3 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts @@ -30,6 +30,8 @@ import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workben import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { isWindows } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; suite('WorkingCopyBackupTracker (browser)', function () { let accessor: TestServiceAccessor; @@ -93,6 +95,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { disposables.add(registerTestResourceEditor()); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); @@ -199,6 +202,7 @@ suite('WorkingCopyBackupTracker (browser)', function () { const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts index 80c239947d4..66cd9ae27e5 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts @@ -6,10 +6,12 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IWorkingCopyEditorHandler, WorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { createEditorPart, registerTestResourceEditor, TestEditorService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; @@ -52,6 +54,7 @@ suite('WorkingCopyEditorService', () => { const instantiationService = workbenchInstantiationService(); const part = await createEditorPart(instantiationService, disposables); instantiationService.stub(IEditorGroupsService, part); + instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(false)); const editorService = instantiationService.createInstance(EditorService); const accessor = instantiationService.createInstance(TestServiceAccessor); diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 7f134cebfa1..45ecccf0680 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -22,10 +22,12 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; import { isSingleFolderWorkspaceIdentifier, isUntitledWorkspace, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt'; +export const WORKSPACE_TRUST_EMPTY_WINDOW = 'security.workspace.trust.emptyWindow'; export const WORKSPACE_TRUST_EXTENSION_SUPPORT = 'extensions.supportUntrustedWorkspaces'; export const WORKSPACE_TRUST_STORAGE_KEY = 'content.trust.model.key'; @@ -53,9 +55,9 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork private readonly _onDidChangeTrustedFolders = this._register(new Emitter()); readonly onDidChangeTrustedFolders = this._onDidChangeTrustedFolders.event; - private _isWorkspaceTrusted: boolean = false; private _trustStateInfo: IWorkspaceTrustInfo; + private readonly _trustState: WorkspaceTrustState; private readonly _trustTransitionManager: WorkspaceTrustTransitionManager; constructor( @@ -67,11 +69,12 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork ) { super(); - this._trustStateInfo = this.loadTrustInfo(); - this._isWorkspaceTrusted = this.calculateWorkspaceTrust(); - + this._trustState = new WorkspaceTrustState(this.storageService); this._trustTransitionManager = this._register(new WorkspaceTrustTransitionManager()); + this._trustStateInfo = this.loadTrustInfo(); + this._trustState.isTrusted = this.calculateWorkspaceTrust(); + this.registerListeners(); } @@ -132,7 +135,9 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { - return true; + // Use memento if present, otherwise default to restricted mode + // Workspace may transition to trusted based on the opened editors + return this._trustState.isTrusted ?? false; } const workspaceUris = this.getWorkspaceUris(); @@ -170,7 +175,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork if (this.isWorkpaceTrusted() === trusted) { return; } // Update workspace trust - this._isWorkspaceTrusted = trusted; + this._trustState.isTrusted = trusted; // Run workspace trust transition participants await this._trustTransitionManager.participate(trusted); @@ -179,6 +184,14 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork this._onDidChangeTrust.fire(trusted); } + get acceptsNonWorkspaceFiles(): boolean { + return this._trustState.acceptsNonWorkspaceFiles; + } + + set acceptsNonWorkspaceFiles(value: boolean) { + this._trustState.acceptsNonWorkspaceFiles = value; + } + addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable { return this._trustTransitionManager.addWorkspaceTrustTransitionParticipant(participant); } @@ -230,7 +243,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork canSetWorkspaceTrust(): boolean { // Empty workspace if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { - return false; + return true; } // Untrusted workspace @@ -269,7 +282,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } isWorkpaceTrusted(): boolean { - return this._isWorkspaceTrusted; + return this._trustState.isTrusted ?? false; } setParentFolderTrust(trusted: boolean): void { @@ -282,6 +295,14 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } async setWorkspaceTrust(trusted: boolean): Promise { + // Empty workspace + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + this._trustState.isTrusted = trusted; + await this.updateWorkspaceTrust(); + + return; + } + const workspaceFolders = this.getWorkspaceUris(); await this.setUrisTrust(workspaceFolders, trusted); } @@ -320,7 +341,6 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa _serviceBrand: undefined; private _trusted!: boolean; - private _acceptingNonWorkspaceFiles!: boolean; private _modalTrustRequestPromise?: Promise; private _modalTrustRequestResolver?: (trusted: boolean | undefined) => void; private readonly _ctxWorkspaceTrustState: IContextKey; @@ -341,8 +361,6 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa this._ctxWorkspaceTrustState = WorkspaceTrustContext.IsTrusted.bindTo(contextKeyService); this.trusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); - - this._acceptingNonWorkspaceFiles = false; } private get trusted(): boolean { @@ -350,10 +368,6 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa } private set trusted(trusted: boolean) { - if (!trusted) { - this._acceptingNonWorkspaceFiles = false; - } - this._trusted = trusted; this._ctxWorkspaceTrustState.set(trusted); } @@ -367,43 +381,6 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa } } - async requestOpenUris(uris: URI[]): Promise { - // If workspace is untrusted, there is no conflict - if (!this.trusted) { - return WorkspaceTrustUriResponse.Open; - } - - const allTrusted = uris.map(uri => { - return this.workspaceTrustManagementService.getUriTrustInfo(uri).trusted; - }).every(trusted => trusted); - - // If all uris are trusted, there is no conflict - if (allTrusted) { - return WorkspaceTrustUriResponse.Open; - } - - // If we already asked the user, don't need to ask again - if (this._acceptingNonWorkspaceFiles) { - return WorkspaceTrustUriResponse.Open; - } - - const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Are you sure you want to open these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Window"), localize('cancel', "Cancel")], { - detail: localize('openLooseFileDetails', "You are trying to open untrusted files into the current window which is trusted. How would you like to continue?"), - cancelId: 2, - custom: { icon: Codicon.shield } - }); - - switch (result.choice) { - case 0: - this._acceptingNonWorkspaceFiles = true; - return WorkspaceTrustUriResponse.Open; - case 1: - return WorkspaceTrustUriResponse.OpenInNewWindow; - default: - return WorkspaceTrustUriResponse.Cancel; - } - } - cancelRequest(): void { if (this._modalTrustRequestResolver) { this._modalTrustRequestResolver(undefined); @@ -424,6 +401,43 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa this.resolveRequest(trusted); } + async requestOpenUris(uris: URI[]): Promise { + // If workspace is untrusted, there is no conflict + if (!this.trusted) { + return WorkspaceTrustUriResponse.Open; + } + + const allTrusted = uris.map(uri => { + return this.workspaceTrustManagementService.getUriTrustInfo(uri).trusted; + }).every(trusted => trusted); + + // If all uris are trusted, there is no conflict + if (allTrusted) { + return WorkspaceTrustUriResponse.Open; + } + + // If we already asked the user, don't need to ask again + if (this.workspaceTrustManagementService.acceptsNonWorkspaceFiles) { + return WorkspaceTrustUriResponse.Open; + } + + const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Are you sure you want to open these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Window"), localize('cancel', "Cancel")], { + detail: localize('openLooseFileDetails', "You are trying to open untrusted files into the current window which is trusted. How would you like to continue?"), + cancelId: 2, + custom: { icon: Codicon.shield } + }); + + switch (result.choice) { + case 0: + this.workspaceTrustManagementService.acceptsNonWorkspaceFiles = true; + return WorkspaceTrustUriResponse.Open; + case 1: + return WorkspaceTrustUriResponse.OpenInNewWindow; + default: + return WorkspaceTrustUriResponse.Cancel; + } + } + async requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { // Trusted workspace if (this.trusted) { @@ -446,7 +460,7 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa } } -export class WorkspaceTrustTransitionManager extends Disposable { +class WorkspaceTrustTransitionManager extends Disposable { private readonly participants = new LinkedList(); @@ -466,4 +480,39 @@ export class WorkspaceTrustTransitionManager extends Disposable { } } +class WorkspaceTrustState { + private readonly _memento: Memento; + private readonly _mementoObject: MementoObject; + + private readonly _acceptsNonWorkspaceFilesKey = 'acceptsNonWorkspaceFiles'; + private readonly _isTrustedKey = 'isTrusted'; + + constructor(storageService: IStorageService) { + this._memento = new Memento('workspaceTrust', storageService); + this._mementoObject = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + get acceptsNonWorkspaceFiles(): boolean { + return this._mementoObject[this._acceptsNonWorkspaceFilesKey] ?? false; + } + + set acceptsNonWorkspaceFiles(value: boolean) { + this._mementoObject[this._acceptsNonWorkspaceFilesKey] = value; + this._memento.saveMemento(); + } + + get isTrusted(): boolean | undefined { + return this._mementoObject[this._isTrustedKey]; + } + + set isTrusted(value: boolean | undefined) { + this._mementoObject[this._isTrustedKey] = value; + if (!value) { + this._mementoObject[this._acceptsNonWorkspaceFilesKey] = value; + } + + this._memento.saveMemento(); + } +} + registerSingleton(IWorkspaceTrustRequestService, WorkspaceTrustRequestService); diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts index 4f25ee6c78e..d6b1f6ac19a 100644 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts @@ -24,6 +24,14 @@ export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManag this.trusted = trusted; } + get acceptsNonWorkspaceFiles(): boolean { + throw new Error('Method not implemented.'); + } + + set acceptsNonWorkspaceFiles(value: boolean) { + throw new Error('Method not implemented.'); + } + addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable { throw new Error('Method not implemented.'); } @@ -80,7 +88,7 @@ export class TestWorkspaceTrustRequestService implements IWorkspaceTrustRequestS constructor(private readonly _trusted: boolean) { } async requestOpenUris(uris: URI[]): Promise { - throw new Error('Method not implemented.'); + return Promise.resolve(WorkspaceTrustUriResponse.Open); } cancelRequest(): void { From 669b0b3f7191375b9904405fa02ed1087c02020e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 20 May 2021 10:56:18 +0200 Subject: [PATCH 187/270] working copy manager - clean up some types add some new APIs on manager2 for unified access: - get(resource): working copy - workingCopies: workingcopy[] - onDidCreate: Event //cc @jrieken --- .../common/abstractFileWorkingCopy.ts | 14 ++-- .../common/abstractFileWorkingCopyManager.ts | 12 +++ .../workingCopy/common/fileWorkingCopy.ts | 26 +++--- .../common/fileWorkingCopyManager.ts | 83 +++++++------------ .../common/fileWorkingCopyManager2.ts | 56 +++++++------ .../common/untitledFileWorkingCopy.ts | 22 ++--- .../common/untitledFileWorkingCopyManager.ts | 37 +++++---- .../browser/fileWorkingCopyManager2.test.ts | 30 +++++++ .../untitledFileWorkingCopyManager.test.ts | 14 ++++ 9 files changed, 168 insertions(+), 126 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts index 3568f1ca66b..b0a95a39ff9 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts @@ -10,7 +10,7 @@ import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; -export interface IBaseFileWorkingCopyModelFactory { +export interface IBaseFileWorkingCopyModelFactory { /** * Create a model from the given content under the provided resource. @@ -19,7 +19,7 @@ export interface IBaseFileWorkingCopyModelFactory; + createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise; } /** @@ -72,7 +72,7 @@ export interface IBaseFileWorkingCopyModel extends IDisposable { update(contents: VSBufferReadableStream, token: CancellationToken): Promise; } -export interface IBaseFileWorkingCopy extends IWorkingCopy, IDisposable { +export interface IBaseFileWorkingCopy extends IWorkingCopy, IDisposable { /** * An event for when the file working copy has been reverted. @@ -89,7 +89,7 @@ export interface IBaseFileWorkingCopy exten * based working copy. As long as the file working copy * has not been resolved, the model is `undefined`. */ - readonly model: T | undefined; + readonly model: M | undefined; /** * Resolves the file working copy and thus makes the `model` @@ -100,13 +100,13 @@ export interface IBaseFileWorkingCopy exten /** * Whether we have a resolved model or not. */ - isResolved(): this is IBaseResolvedFileWorkingCopy; + isResolved(): this is IBaseResolvedFileWorkingCopy; } -export interface IBaseResolvedFileWorkingCopy extends IBaseFileWorkingCopy { +export interface IBaseResolvedFileWorkingCopy extends IBaseFileWorkingCopy { /** * A resolved file working copy has a resolved model. */ - readonly model: T; + readonly model: M; } diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 0659e102304..93f7248e891 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { Promises } from 'vs/base/common/async'; @@ -14,6 +15,11 @@ import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/se export interface IBaseFileWorkingCopyManager> extends IDisposable { + /** + * An event for when a file working copy was created. + */ + readonly onDidCreate: Event; + /** * Access to all known file working copies within the manager. */ @@ -40,6 +46,9 @@ export interface IBaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { + private readonly _onDidCreate = this._register(new Emitter()); + readonly onDidCreate = this._onDidCreate.event; + private readonly mapResourceToWorkingCopy = new ResourceMap(); private readonly mapResourceToDisposeListener = new ResourceMap(); @@ -67,6 +76,9 @@ export abstract class BaseFileWorkingCopyManager this.remove(resource))); + + // Signal creation event + this._onDidCreate.fire(workingCopy); } protected remove(resource: URI): void { diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index 5a7e140a6d8..b81bc57845f 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -32,7 +32,7 @@ import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel, IBaseFileWorkingCopyMo /** * File specific working copy model factory. */ -export interface IFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } +export interface IFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } /** * The underlying model of a file working copy provides some @@ -89,7 +89,7 @@ export interface IFileWorkingCopyModelContentChangedEvent { * of functionality can be built on top, such as saving in * a secure way to prevent data loss. */ -export interface IFileWorkingCopy extends IResourceWorkingCopy, IBaseFileWorkingCopy { +export interface IFileWorkingCopy extends IResourceWorkingCopy, IBaseFileWorkingCopy { /** * An event for when a file working copy was resolved. @@ -140,7 +140,7 @@ export interface IFileWorkingCopy extends IReso /** * Whether we have a resolved model or not. */ - isResolved(): this is IResolvedFileWorkingCopy; + isResolved(): this is IResolvedFileWorkingCopy; /** * Whether the file working copy is readonly or not. @@ -148,12 +148,12 @@ export interface IFileWorkingCopy extends IReso isReadonly(): boolean; } -export interface IResolvedFileWorkingCopy extends IFileWorkingCopy { +export interface IResolvedFileWorkingCopy extends IFileWorkingCopy { /** * A resolved file working copy has a resolved model. */ - readonly model: T; + readonly model: M; } /** @@ -255,12 +255,12 @@ interface IFileWorkingCopyBackupMetaData extends IWorkingCopyBackupMeta { orphaned: boolean; } -export class FileWorkingCopy extends ResourceWorkingCopy implements IFileWorkingCopy { +export class FileWorkingCopy extends ResourceWorkingCopy implements IFileWorkingCopy { readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.None; - private _model: T | undefined = undefined; - get model(): T | undefined { return this._model; } + private _model: M | undefined = undefined; + get model(): M | undefined { return this._model; } //#region events @@ -291,7 +291,7 @@ export class FileWorkingCopy extends ResourceWo readonly typeId: string, resource: URI, readonly name: string, - private readonly modelFactory: IFileWorkingCopyModelFactory, + private readonly modelFactory: IFileWorkingCopyModelFactory, @IFileService fileService: IFileService, @ILogService private readonly logService: ILogService, @ITextFileService private readonly textFileService: ITextFileService, @@ -318,7 +318,7 @@ export class FileWorkingCopy extends ResourceWo private dirty = false; private savedVersionId: unknown; - isDirty(): this is IResolvedFileWorkingCopy { + isDirty(): this is IResolvedFileWorkingCopy { return this.dirty; } @@ -379,7 +379,7 @@ export class FileWorkingCopy extends ResourceWo private lastResolvedFileStat: IFileStatWithMetadata | undefined; - isResolved(): this is IResolvedFileWorkingCopy { + isResolved(): this is IResolvedFileWorkingCopy { return !!this.model; } @@ -640,7 +640,7 @@ export class FileWorkingCopy extends ResourceWo } } - private installModelListeners(model: IFileWorkingCopyModel): void { + private installModelListeners(model: M): void { // See https://github.com/microsoft/vscode/issues/30189 // This code has been extracted to a different method because it caused a memory leak @@ -653,7 +653,7 @@ export class FileWorkingCopy extends ResourceWo this._register(model.onWillDispose(() => this.dispose())); } - private onModelContentChanged(model: IFileWorkingCopyModel, isUndoingOrRedoing: boolean): void { + private onModelContentChanged(model: M, isUndoingOrRedoing: boolean): void { this.trace(`[file working copy] onModelContentChanged() - enter`); // In any case increment the version id because it tracks the textual content state of the model at all times diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index f1c7354fa8b..01deb13336b 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -5,7 +5,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { SaveReason } from 'vs/workbench/common/editor'; import { ResourceMap } from 'vs/base/common/map'; import { Promises, ResourceQueue } from 'vs/base/common/async'; @@ -29,37 +29,32 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; * operations that are working copy related, such as save/revert, backup * and resolving. */ -export interface IFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { - - /** - * An event for when a file working copy was created. - */ - readonly onDidCreate: Event>; +export interface IFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { /** * An event for when a file working copy was resolved. */ - readonly onDidResolve: Event>; + readonly onDidResolve: Event>; /** * An event for when a file working copy changed it's dirty state. */ - readonly onDidChangeDirty: Event>; + readonly onDidChangeDirty: Event>; /** * An event for when a file working copy failed to save. */ - readonly onDidSaveError: Event>; + readonly onDidSaveError: Event>; /** * An event for when a file working copy successfully saved. */ - readonly onDidSave: Event>; + readonly onDidSave: Event>; /** * An event for when a file working copy was reverted. */ - readonly onDidRevert: Event>; + readonly onDidRevert: Event>; /** * Allows to resolve a file working copy. If the manager already knows @@ -78,29 +73,29 @@ export interface IFileWorkingCopyManager extend * case one is already known for this `URI`. * @param options */ - resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; + resolve(resource: URI, options?: IFileWorkingCopyManagerResolveOptions): Promise>; /** * Waits for the file working copy to be ready to be disposed. There may be * conditions under which the file working copy cannot be disposed, e.g. when * it is dirty. Once the promise is settled, it is safe to dispose. */ - canDispose(workingCopy: IFileWorkingCopy): true | Promise; + canDispose(workingCopy: IFileWorkingCopy): true | Promise; /** * @deprecated */ - saveAs(source: URI, target: URI): Promise | undefined>; - saveAs(source: URI, target: undefined): Promise | undefined>; + saveAs(source: URI, target: URI): Promise | undefined>; + saveAs(source: URI, target: undefined): Promise | undefined>; } -export interface IFileWorkingCopySaveEvent { +export interface IFileWorkingCopySaveEvent { /** * The file working copy that was successfully saved. */ - workingCopy: IFileWorkingCopy; + workingCopy: IFileWorkingCopy; /** * The reason why the file working copy was saved. @@ -108,17 +103,7 @@ export interface IFileWorkingCopySaveEvent { reason: SaveReason; } -export interface IFileWorkingCopyResolveOptions { - - /** - * The contents to use for the file working copy if known. - * If not provided, the contents will be retrieved from the - * underlying resource or backup if present. - * - * If contents are provided, the file working copy will be marked - * as dirty right from the beginning. - */ - contents?: VSBufferReadableStream; +export interface IFileWorkingCopyManagerResolveOptions extends IFileWorkingCopyResolveOptions { /** * If the file working copy was already resolved before, @@ -133,26 +118,23 @@ export interface IFileWorkingCopyResolveOptions { }; } -export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { +export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { //#region Events - private readonly _onDidCreate = this._register(new Emitter>()); - readonly onDidCreate = this._onDidCreate.event; - - private readonly _onDidResolve = this._register(new Emitter>()); + private readonly _onDidResolve = this._register(new Emitter>()); readonly onDidResolve = this._onDidResolve.event; - private readonly _onDidChangeDirty = this._register(new Emitter>()); + private readonly _onDidChangeDirty = this._register(new Emitter>()); readonly onDidChangeDirty = this._onDidChangeDirty.event; - private readonly _onDidSaveError = this._register(new Emitter>()); + private readonly _onDidSaveError = this._register(new Emitter>()); readonly onDidSaveError = this._onDidSaveError.event; - private readonly _onDidSave = this._register(new Emitter>()); + private readonly _onDidSave = this._register(new Emitter>()); readonly onDidSave = this._onDidSave.event; - private readonly _onDidRevert = this._register(new Emitter>()); + private readonly _onDidRevert = this._register(new Emitter>()); readonly onDidRevert = this._onDidRevert.event; //#endregion @@ -164,7 +146,7 @@ export class FileWorkingCopyManager extends Bas constructor( private readonly workingCopyTypeId: string, - private readonly modelFactory: IFileWorkingCopyModelFactory, + private readonly modelFactory: IFileWorkingCopyModelFactory, @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, @@ -195,7 +177,7 @@ export class FileWorkingCopyManager extends Bas } private async onWillShutdown(): Promise { - let fileWorkingCopies: IFileWorkingCopy[]; + let fileWorkingCopies: IFileWorkingCopy[]; // As long as file working copies are pending to be saved, we prolong the shutdown // until that has happened to ensure we are not shutting down in the middle of @@ -223,7 +205,7 @@ export class FileWorkingCopyManager extends Bas } } - private queueWorkingCopyResolve(workingCopy: IFileWorkingCopy): void { + private queueWorkingCopyResolve(workingCopy: IFileWorkingCopy): void { // Resolves a working copy to update (use a queue to prevent accumulation of // resolve when the resolving actually takes long. At most we only want the @@ -260,7 +242,7 @@ export class FileWorkingCopyManager extends Bas } // Find all working copies that related to source (can be many if resource is a folder) - const sourceWorkingCopies: IFileWorkingCopy[] = []; + const sourceWorkingCopies: IFileWorkingCopy[] = []; for (const workingCopy of this.workingCopies) { if (this.uriIdentityService.extUri.isEqualOrParent(workingCopy.resource, source)) { sourceWorkingCopies.push(workingCopy); @@ -364,7 +346,7 @@ export class FileWorkingCopyManager extends Bas //#region Resolve - async resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise> { + async resolve(resource: URI, options?: IFileWorkingCopyManagerResolveOptions): Promise> { // Await a pending working copy resolve first before proceeding // to ensure that we never resolve a working copy more than once @@ -417,7 +399,7 @@ export class FileWorkingCopyManager extends Bas resource, this.labelService.getUriBasenameLabel(resource), this.modelFactory - ) as unknown as IFileWorkingCopy; + ) as unknown as IFileWorkingCopy; workingCopyResolve = workingCopy.resolve(options); @@ -432,7 +414,6 @@ export class FileWorkingCopyManager extends Bas // Emit some events if we created the working copy if (didCreateWorkingCopy) { - this._onDidCreate.fire(workingCopy); // If the working copy is dirty right from the beginning, // make sure to emit this as an event @@ -479,7 +460,7 @@ export class FileWorkingCopyManager extends Bas return undefined; } - private registerWorkingCopy(workingCopy: IFileWorkingCopy): void { + private registerWorkingCopy(workingCopy: IFileWorkingCopy): void { // Install working copy listeners const workingCopyListeners = new DisposableStore(); @@ -508,7 +489,7 @@ export class FileWorkingCopyManager extends Bas //#region Lifecycle - canDispose(workingCopy: IFileWorkingCopy): true | Promise { + canDispose(workingCopy: IFileWorkingCopy): true | Promise { // Quick return if working copy already disposed or not dirty and not resolving if ( @@ -522,7 +503,7 @@ export class FileWorkingCopyManager extends Bas return this.doCanDispose(workingCopy); } - private async doCanDispose(workingCopy: IFileWorkingCopy): Promise { + private async doCanDispose(workingCopy: IFileWorkingCopy): Promise { // If we have a pending working copy resolve, await it first and then try again const pendingResolve = this.joinPendingResolve(workingCopy.resource); @@ -563,7 +544,7 @@ export class FileWorkingCopyManager extends Bas /** * @deprecated TODO@bpasero remove */ - async saveAs(source: URI, target?: URI): Promise | undefined> { + async saveAs(source: URI, target?: URI): Promise | undefined> { // If not provided, ask user for target if (!target) { @@ -578,7 +559,7 @@ export class FileWorkingCopyManager extends Bas return this.doSaveAs(source, target); } - private async doSaveAs(source: URI, target: URI): Promise | undefined> { + private async doSaveAs(source: URI, target: URI): Promise | undefined> { let sourceContents: VSBufferReadableStream; // If the source is an existing file working copy, we can directly @@ -598,7 +579,7 @@ export class FileWorkingCopyManager extends Bas return this.doSaveAsWorkingCopy(source, sourceContents, target); } - private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI): Promise> { + private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI): Promise> { // Prefer an existing working copy if it is already resolved // for the given target resource diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts index 32b0932927d..2a21403c4f3 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; +import { Event } from 'vs/base/common/event'; import { Promises } from 'vs/base/common/async'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { toLocalResource, joinPath, isEqual, basename, dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IFileDialogService, IDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; @@ -18,13 +19,15 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; -import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { FileWorkingCopyManager, IFileWorkingCopyManager, IFileWorkingCopyManagerResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { INewOrExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyWithAssociatedResourceOptions, IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { isValidBasename } from 'vs/base/common/extpath'; +import { IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; +import { IBaseFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; -export interface IFileWorkingCopyManager2 extends IDisposable { +export interface IFileWorkingCopyManager2 extends IBaseFileWorkingCopyManager> { /** * Provides access to the manager for titled file working copies. @@ -52,7 +55,7 @@ export interface IFileWorkingCopyManager2>; + resolve(resource: URI, options?: IFileWorkingCopyManagerResolveOptions): Promise>; /** * Create a new untitled file working copy with optional initial contents. @@ -101,20 +104,8 @@ export interface IFileWorkingCopyManager2 | undefined>; - saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; - - /** - * Disposes all working copies of the manager and disposes the manager. This - * method is different from `dispose` in that it will unregister any working - * copy from the `IWorkingCopyService`. Since this impact things like backups, - * the method is `async` because it needs to trigger `save` for any dirty - * working copy to preserve the data. - * - * Callers should make sure to e.g. close any editors associated with the - * working copy. - */ - destroy(): Promise; + saveAs(source: URI, target: URI, options?: ISaveOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; } export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { @@ -128,6 +119,8 @@ export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { export class FileWorkingCopyManager2 extends Disposable implements IFileWorkingCopyManager2 { + readonly onDidCreate: Event>; + readonly files: IFileWorkingCopyManager; readonly untitled: IUntitledFileWorkingCopyManager; @@ -155,8 +148,23 @@ export class FileWorkingCopyManager2; + + // Events + this.onDidCreate = Event.any>(this.files.onDidCreate, this.untitled.onDidCreate); } + //#region get / get all + + get workingCopies(): (IUntitledFileWorkingCopy | IFileWorkingCopy)[] { + return [...this.files.workingCopies, ...this.untitled.workingCopies]; + } + + get(resource: URI): IUntitledFileWorkingCopy | IFileWorkingCopy | undefined { + return this.files.get(resource) ?? this.untitled.get(resource); + } + + //#endregion + //#region resolve resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; @@ -175,7 +183,7 @@ export class FileWorkingCopyManager2 | undefined> { + async saveAs(source: URI, target?: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { // Get to target resource if (!target) { @@ -217,7 +225,7 @@ export class FileWorkingCopyManager2 | undefined> { + private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { // Save is only possible with file working copies, // any other have to go via `saveAs` flow. @@ -232,7 +240,7 @@ export class FileWorkingCopyManager2 | undefined> { + private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { let sourceContents: VSBufferReadableStream; // If the source is an existing file working copy, we can directly @@ -278,11 +286,7 @@ export class FileWorkingCopyManager2 | IFileWorkingCopy | undefined { - return this.files.get(resource) ?? this.untitled.get(resource); - } - - private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetFileWorkingCopy: IFileWorkingCopy }> { + private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetFileWorkingCopy: IFileWorkingCopy }> { // Prefer an existing file working copy if it is already resolved // for the given target resource diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 90d24089bbb..74f77fc9b1c 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -21,7 +21,7 @@ import { emptyStream } from 'vs/base/common/stream'; /** * Untitled file specific working copy model factory. */ -export interface IUntitledFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } +export interface IUntitledFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } /** * The underlying model of a untitled file working copy provides @@ -45,7 +45,7 @@ export interface IUntitledFileWorkingCopyModelContentChangedEvent { readonly isEmpty: boolean; } -export interface IUntitledFileWorkingCopy extends IBaseFileWorkingCopy { +export interface IUntitledFileWorkingCopy extends IBaseFileWorkingCopy { /** * Whether this untitled file working copy model has an associated file path. @@ -55,15 +55,15 @@ export interface IUntitledFileWorkingCopy; + isResolved(): this is IResolvedUntitledFileWorkingCopy; } -export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { +export interface IResolvedUntitledFileWorkingCopy extends IUntitledFileWorkingCopy { /** * A resolved untitled file working copy has a resolved model. */ - readonly model: T; + readonly model: M; } export interface IUntitledFileWorkingCopySaveDelegate { @@ -74,12 +74,12 @@ export interface IUntitledFileWorkingCopySaveDelegate { (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; } -export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { +export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.Untitled; - private _model: T | undefined = undefined; - get model(): T | undefined { return this._model; } + private _model: M | undefined = undefined; + get model(): M | undefined { return this._model; } //#region Events @@ -103,7 +103,7 @@ export class UntitledFileWorkingCopy ex readonly name: string, readonly hasAssociatedFilePath: boolean, private readonly initialValue: VSBufferReadableStream | undefined, - private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, + private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @@ -194,7 +194,7 @@ export class UntitledFileWorkingCopy ex this.installModelListeners(this._model); } - private installModelListeners(model: IUntitledFileWorkingCopyModel): void { + private installModelListeners(model: M): void { // Content Change this._register(model.onDidChangeContent(e => this.onModelContentChanged(e))); @@ -221,7 +221,7 @@ export class UntitledFileWorkingCopy ex this._onDidChangeContent.fire(); } - isResolved(): this is IResolvedUntitledFileWorkingCopy { + isResolved(): this is IResolvedUntitledFileWorkingCopy { return !!this.model; } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 8b326b5dadd..53f8202eee9 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -22,24 +22,24 @@ import { ResourceMap } from 'vs/base/common/map'; * handle all operations that are working copy related, such as save/revert, * backup and resolving. */ -export interface IUntitledFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { +export interface IUntitledFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { /** * An event for when a untitled file working copy changed it's dirty state. */ - readonly onDidChangeDirty: Event>; + readonly onDidChangeDirty: Event>; /** * An event for when a untitled file working copy is about to be disposed. */ - readonly onWillDispose: Event>; + readonly onWillDispose: Event>; /** * Create a new untitled file working copy with optional initial contents. * * Note: Callers must `dispose` the working copy when no longer needed. */ - resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; /** * Create a new untitled file working copy with optional initial contents @@ -48,7 +48,7 @@ export interface IUntitledFileWorkingCopyManager>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; /** * Creates a new untitled file working copy with optional initial contents @@ -57,7 +57,7 @@ export interface IUntitledFileWorkingCopyManager>; + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; } export interface INewUntitledFileWorkingCopyOptions { @@ -97,14 +97,14 @@ export interface INewOrExistingUntitledFileWorkingCopyOptions extends INewUntitl type IInternalUntitledFileWorkingCopyOptions = INewUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions & INewOrExistingUntitledFileWorkingCopyOptions; -export class UntitledFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IUntitledFileWorkingCopyManager { +export class UntitledFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IUntitledFileWorkingCopyManager { //#region Events - private readonly _onDidChangeDirty = this._register(new Emitter>()); + private readonly _onDidChangeDirty = this._register(new Emitter>()); readonly onDidChangeDirty = this._onDidChangeDirty.event; - private readonly _onWillDispose = this._register(new Emitter>()); + private readonly _onWillDispose = this._register(new Emitter>()); readonly onWillDispose = this._onWillDispose.event; //#endregion @@ -113,7 +113,7 @@ export class UntitledFileWorkingCopyManager, + private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IFileService fileService: IFileService, @ILabelService private readonly labelService: ILabelService, @@ -126,17 +126,17 @@ export class UntitledFileWorkingCopyManager>; - resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; - resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; - async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { + resolve(options?: INewUntitledFileWorkingCopyOptions): Promise>; + resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; + resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; + async resolve(options?: IInternalUntitledFileWorkingCopyOptions): Promise> { const workingCopy = this.doCreateOrGet(options); await workingCopy.resolve(); return workingCopy; } - private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { + private doCreateOrGet(options: IInternalUntitledFileWorkingCopyOptions = Object.create(null)): IUntitledFileWorkingCopy { const massagedOptions = this.massageOptions(options); // Return existing instance if asked for it @@ -177,7 +177,7 @@ export class UntitledFileWorkingCopyManager { + private doCreate(options: IInternalUntitledFileWorkingCopyOptions): IUntitledFileWorkingCopy { // Create a new untitled resource if none is provided let untitledResource = options.untitledResource; @@ -199,14 +199,15 @@ export class UntitledFileWorkingCopyManager; + ) as unknown as IUntitledFileWorkingCopy; + // Register this.registerWorkingCopy(workingCopy); return workingCopy; } - private registerWorkingCopy(workingCopy: IUntitledFileWorkingCopy): void { + private registerWorkingCopy(workingCopy: IUntitledFileWorkingCopy): void { // Install working copy listeners const workingCopyListeners = new DisposableStore(); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts index 9ec0f1f82b2..d7e15bd7666 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts @@ -48,6 +48,36 @@ suite('FileWorkingCopyManager2', () => { manager.dispose(); }); + test('onDidCreate, get, workingCopies', async () => { + let createCounter = 0; + manager.onDidCreate(e => { + createCounter++; + }); + + const fileUri = URI.file('/test.html'); + + assert.strictEqual(manager.workingCopies.length, 0); + assert.strictEqual(manager.get(fileUri), undefined); + + const fileWorkingCopy = await manager.resolve(fileUri); + const untitledFileWorkingCopy = await manager.resolve(); + + assert.strictEqual(manager.workingCopies.length, 2); + assert.strictEqual(createCounter, 2); + assert.strictEqual(manager.get(fileWorkingCopy.resource), fileWorkingCopy); + assert.strictEqual(manager.get(untitledFileWorkingCopy.resource), untitledFileWorkingCopy); + + const sameFileWorkingCopy = await manager.resolve(fileUri); + const sameUntitledFileWorkingCopy = await manager.resolve({ untitledResource: untitledFileWorkingCopy.resource }); + assert.strictEqual(sameFileWorkingCopy, fileWorkingCopy); + assert.strictEqual(sameUntitledFileWorkingCopy, untitledFileWorkingCopy); + assert.strictEqual(manager.workingCopies.length, 2); + assert.strictEqual(createCounter, 2); + + fileWorkingCopy.dispose(); + untitledFileWorkingCopy.dispose(); + }); + test('resolve', async () => { const fileWorkingCopy = await manager.resolve(URI.file('/test.html')); assert.ok(fileWorkingCopy instanceof FileWorkingCopy); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 5acba50cecc..2b74144b657 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -48,6 +48,11 @@ suite('UntitledFileWorkingCopyManager', () => { }); test('basics', async () => { + let createCounter = 0; + manager.untitled.onDidCreate(e => { + createCounter++; + }); + let disposeCounter = 0; manager.untitled.onWillDispose(e => { disposeCounter++; @@ -67,6 +72,8 @@ suite('UntitledFileWorkingCopyManager', () => { const workingCopy1 = await manager.untitled.resolve(); const workingCopy2 = await manager.untitled.resolve(); + assert.strictEqual(createCounter, 2); + assert.strictEqual(manager.untitled.get(workingCopy1.resource), workingCopy1); assert.strictEqual(manager.untitled.get(workingCopy2.resource), workingCopy2); @@ -123,10 +130,17 @@ suite('UntitledFileWorkingCopyManager', () => { }); test('resolve - existing', async () => { + let createCounter = 0; + manager.untitled.onDidCreate(e => { + createCounter++; + }); + const workingCopy1 = await manager.untitled.resolve(); + assert.strictEqual(createCounter, 1); const workingCopy2 = await manager.untitled.resolve({ untitledResource: workingCopy1.resource }); assert.strictEqual(workingCopy1, workingCopy2); + assert.strictEqual(createCounter, 1); const workingCopy3 = await manager.untitled.resolve({ untitledResource: URI.file('/invalid/untitled') }); assert.strictEqual(workingCopy3.resource.scheme, Schemas.untitled); From 1543754dcab161f981cd50979d01591aa6745115 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 20 May 2021 11:44:45 +0200 Subject: [PATCH 188/270] Tweak loose file dialog warning --- .../workspaces/common/workspaceTrust.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 45ecccf0680..356c393a835 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -5,6 +5,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { splitName } from 'vs/base/common/labels'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; @@ -352,7 +353,8 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa constructor( @IContextKeyService contextKeyService: IContextKeyService, @IDialogService private readonly dialogService: IDialogService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { super(); @@ -421,10 +423,19 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa return WorkspaceTrustUriResponse.Open; } - const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Are you sure you want to open these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Window"), localize('cancel', "Cancel")], { - detail: localize('openLooseFileDetails', "You are trying to open untrusted files into the current window which is trusted. How would you like to continue?"), + const markdownDetails = [ + this.workspaceService.getWorkbenchState() !== WorkbenchState.EMPTY ? + localize('openLooseFileWorkspaceDetails', "You are trying to open untrusted files in a workspace which is trusted.") : + localize('openLooseFileWindowDetails', "You are trying to open untrusted files in a window which is trusted."), + localize('openLooseFileLearnMore', "If you don't trust the authors of these files, we recommend to open them in a new Restricted Mode window as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.") + ]; + + const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Do you trust the authors of these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Restricted Mode Window"), localize('cancel', "Cancel")], { cancelId: 2, - custom: { icon: Codicon.shield } + custom: { + icon: Codicon.shield, + markdownDetails: markdownDetails.map(md => { return { markdown: new MarkdownString(md) }; }) + } }); switch (result.choice) { From 91bf932633412b99a598b6949443367cda513a72 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 20 May 2021 11:53:19 +0200 Subject: [PATCH 189/270] fixes #123469 --- src/vs/workbench/contrib/files/browser/fileImportExport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 4730977dce3..a0a072c5cbc 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -165,7 +165,7 @@ export class BrowserFileUpload { return; } - await this.explorerService.applyBulkEdit([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true })], { + await this.explorerService.applyBulkEdit([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true, folder: target.getChild(entry.name)?.isDirectory })], { undoLabel: localize('overwrite', "Overwrite {0}", entry.name), progressLabel: localize('overwriting', "Overwriting {0}", entry.name), }); From 234136b6c28420a6a8cbb1feb51e6b39f3282e60 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 20 May 2021 11:54:06 +0200 Subject: [PATCH 190/270] =?UTF-8?q?=F0=9F=92=84=20acceptsNonWorkspaceFiles?= =?UTF-8?q?=20->=20acceptsOutOfWorkspaceFiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/common/workspaceTrust.ts | 4 ++-- .../workspaces/common/workspaceTrust.ts | 24 +++++++++---------- .../test/common/testWorkspaceTrustService.ts | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 6a84d09b027..343273375c1 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -41,8 +41,8 @@ export interface IWorkspaceTrustManagementService { onDidChangeTrust: WorkspaceTrustChangeEvent; onDidChangeTrustedFolders: Event; - get acceptsNonWorkspaceFiles(): boolean; - set acceptsNonWorkspaceFiles(value: boolean); + get acceptsOutOfWorkspaceFiles(): boolean; + set acceptsOutOfWorkspaceFiles(value: boolean); addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable; isWorkpaceTrusted(): boolean; canSetParentFolderTrust(): boolean; diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 356c393a835..82b617dc584 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -185,12 +185,12 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork this._onDidChangeTrust.fire(trusted); } - get acceptsNonWorkspaceFiles(): boolean { - return this._trustState.acceptsNonWorkspaceFiles; + get acceptsOutOfWorkspaceFiles(): boolean { + return this._trustState.acceptsOutOfWorkspaceFiles; } - set acceptsNonWorkspaceFiles(value: boolean) { - this._trustState.acceptsNonWorkspaceFiles = value; + set acceptsOutOfWorkspaceFiles(value: boolean) { + this._trustState.acceptsOutOfWorkspaceFiles = value; } addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable { @@ -419,7 +419,7 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa } // If we already asked the user, don't need to ask again - if (this.workspaceTrustManagementService.acceptsNonWorkspaceFiles) { + if (this.workspaceTrustManagementService.acceptsOutOfWorkspaceFiles) { return WorkspaceTrustUriResponse.Open; } @@ -440,7 +440,7 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa switch (result.choice) { case 0: - this.workspaceTrustManagementService.acceptsNonWorkspaceFiles = true; + this.workspaceTrustManagementService.acceptsOutOfWorkspaceFiles = true; return WorkspaceTrustUriResponse.Open; case 1: return WorkspaceTrustUriResponse.OpenInNewWindow; @@ -495,7 +495,7 @@ class WorkspaceTrustState { private readonly _memento: Memento; private readonly _mementoObject: MementoObject; - private readonly _acceptsNonWorkspaceFilesKey = 'acceptsNonWorkspaceFiles'; + private readonly _acceptsOutOfWorkspaceFilesKey = 'acceptsOutOfWorkspaceFiles'; private readonly _isTrustedKey = 'isTrusted'; constructor(storageService: IStorageService) { @@ -503,12 +503,12 @@ class WorkspaceTrustState { this._mementoObject = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); } - get acceptsNonWorkspaceFiles(): boolean { - return this._mementoObject[this._acceptsNonWorkspaceFilesKey] ?? false; + get acceptsOutOfWorkspaceFiles(): boolean { + return this._mementoObject[this._acceptsOutOfWorkspaceFilesKey] ?? false; } - set acceptsNonWorkspaceFiles(value: boolean) { - this._mementoObject[this._acceptsNonWorkspaceFilesKey] = value; + set acceptsOutOfWorkspaceFiles(value: boolean) { + this._mementoObject[this._acceptsOutOfWorkspaceFilesKey] = value; this._memento.saveMemento(); } @@ -519,7 +519,7 @@ class WorkspaceTrustState { set isTrusted(value: boolean | undefined) { this._mementoObject[this._isTrustedKey] = value; if (!value) { - this._mementoObject[this._acceptsNonWorkspaceFilesKey] = value; + this._mementoObject[this._acceptsOutOfWorkspaceFilesKey] = value; } this._memento.saveMemento(); diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts index d6b1f6ac19a..083c35595c4 100644 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts @@ -24,11 +24,11 @@ export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManag this.trusted = trusted; } - get acceptsNonWorkspaceFiles(): boolean { + get acceptsOutOfWorkspaceFiles(): boolean { throw new Error('Method not implemented.'); } - set acceptsNonWorkspaceFiles(value: boolean) { + set acceptsOutOfWorkspaceFiles(value: boolean) { throw new Error('Method not implemented.'); } From c8410ece1470ff0f6f0c016115e944b21536d919 Mon Sep 17 00:00:00 2001 From: Andre Weinand Date: Thu, 20 May 2021 12:15:07 +0200 Subject: [PATCH 191/270] node-debug@1.44.28 --- product.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product.json b/product.json index 03cff420b86..a16e67696ef 100644 --- a/product.json +++ b/product.json @@ -33,7 +33,7 @@ "builtInExtensions": [ { "name": "ms-vscode.node-debug", - "version": "1.44.27", + "version": "1.44.28", "repo": "https://github.com/microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", From e538fd90c0e78913816047886a5a05810011fa26 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 May 2021 05:07:31 -0700 Subject: [PATCH 192/270] Hover hover when context menu is shown Fixes #124185 --- src/vs/platform/contextview/browser/contextMenuService.ts | 3 +++ src/vs/platform/contextview/browser/contextView.ts | 3 +++ .../contextmenu/electron-sandbox/contextmenuService.ts | 7 +++++++ src/vs/workbench/services/hover/browser/hoverService.ts | 6 ++++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/contextview/browser/contextMenuService.ts b/src/vs/platform/contextview/browser/contextMenuService.ts index 77c92d6dd26..afc0b1c930c 100644 --- a/src/vs/platform/contextview/browser/contextMenuService.ts +++ b/src/vs/platform/contextview/browser/contextMenuService.ts @@ -12,12 +12,15 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Disposable } from 'vs/base/common/lifecycle'; import { ModifierKeyEmitter } from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; export class ContextMenuService extends Disposable implements IContextMenuService { declare readonly _serviceBrand: undefined; private contextMenuHandler: ContextMenuHandler; + readonly onDidShowContextMenu = new Emitter().event; + constructor( @ITelemetryService telemetryService: ITelemetryService, @INotificationService notificationService: INotificationService, diff --git a/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index b33d652cf12..9a37ae69038 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/src/vs/platform/contextview/browser/contextView.ts @@ -7,6 +7,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { AnchorAlignment, AnchorAxisAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { Event } from 'vs/base/common/event'; export const IContextViewService = createDecorator('contextViewService'); @@ -40,5 +41,7 @@ export interface IContextMenuService { readonly _serviceBrand: undefined; + readonly onDidShowContextMenu: Event; + showContextMenu(delegate: IContextMenuDelegate): void; } diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index deefcdc30ba..b6ef5caeb41 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -25,6 +25,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { stripIcons } from 'vs/base/common/iconLabels'; import { coalesce } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; export class ContextMenuService extends Disposable implements IContextMenuService { @@ -32,6 +33,9 @@ export class ContextMenuService extends Disposable implements IContextMenuServic private impl: IContextMenuService; + private readonly _onDidShowContextMenu = this._register(new Emitter()); + readonly onDidShowContextMenu = this._onDidShowContextMenu.event; + constructor( @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @@ -56,6 +60,7 @@ export class ContextMenuService extends Disposable implements IContextMenuServic showContextMenu(delegate: IContextMenuDelegate): void { this.impl.showContextMenu(delegate); + this._onDidShowContextMenu.fire(); } } @@ -63,6 +68,8 @@ class NativeContextMenuService extends Disposable implements IContextMenuService declare readonly _serviceBrand: undefined; + readonly onDidShowContextMenu = new Emitter().event; + constructor( @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 0e1b809311c..03039126b11 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -8,7 +8,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService, IHoverOptions } from 'vs/workbench/services/hover/browser/hover'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { HoverWidget } from 'vs/workbench/services/hover/browser/hoverWidget'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; @@ -21,8 +21,10 @@ export class HoverService implements IHoverService { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IContextViewService private readonly _contextViewService: IContextViewService + @IContextViewService private readonly _contextViewService: IContextViewService, + @IContextMenuService contextMenuService: IContextMenuService ) { + contextMenuService.onDidShowContextMenu(() => this.hideHover()); } showHover(options: IHoverOptions, focus?: boolean): IDisposable | undefined { From da4fcc266fc68103b6e8b6e932f5df786cb6e82c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 20 May 2021 14:15:25 +0200 Subject: [PATCH 193/270] file working copy - avoid ugly casts --- .../common/abstractFileWorkingCopyManager.ts | 2 +- .../common/fileWorkingCopyManager.ts | 28 +++++++--- .../common/fileWorkingCopyManager2.ts | 51 +++++++++++++++---- .../common/untitledFileWorkingCopy.ts | 6 +-- .../common/untitledFileWorkingCopyManager.ts | 18 ++++--- .../browser/fileWorkingCopyManager.test.ts | 13 ++--- .../browser/fileWorkingCopyManager2.test.ts | 13 ++--- .../untitledFileWorkingCopyManager.test.ts | 13 ++--- 8 files changed, 90 insertions(+), 54 deletions(-) diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index 93f7248e891..e95baec1a0a 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -55,7 +55,7 @@ export abstract class BaseFileWorkingCopyManager extends Bas @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IFileDialogService private readonly fileDialogService: IFileDialogService + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @ITextFileService private readonly textFileService: ITextFileService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @INotificationService private readonly notificationService: INotificationService, + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IEditorService private readonly editorService: IEditorService, + @IElevatedFileService private readonly elevatedFileService: IElevatedFileService ) { super(fileService, logService, workingCopyBackupService); @@ -393,13 +405,15 @@ export class FileWorkingCopyManager extends Bas else { didCreateWorkingCopy = true; - workingCopy = this.instantiationService.createInstance( - FileWorkingCopy, + workingCopy = new FileWorkingCopy( this.workingCopyTypeId, resource, this.labelService.getUriBasenameLabel(resource), - this.modelFactory - ) as unknown as IFileWorkingCopy; + this.modelFactory, + this.fileService, this.logService, this.textFileService, this.filesConfigurationService, + this.workingCopyBackupService, this.workingCopyService, this.notificationService, this.workingCopyEditorService, + this.editorService, this.elevatedFileService + ); workingCopyResolve = workingCopy.resolve(options); diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts index 2a21403c4f3..50e8b41d4da 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts @@ -13,7 +13,6 @@ import { toLocalResource, joinPath, isEqual, basename, dirname } from 'vs/base/c import { URI } from 'vs/base/common/uri'; import { IFileDialogService, IDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ISaveOptions } from 'vs/workbench/common/editor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -26,6 +25,17 @@ import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/commo import { isValidBasename } from 'vs/base/common/extpath'; import { IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; import { IBaseFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export interface IFileWorkingCopyManager2 extends IBaseFileWorkingCopyManager> { @@ -129,25 +139,46 @@ export class FileWorkingCopyManager2, private readonly untitledFileModelFactory: IUntitledFileWorkingCopyModelFactory, @IFileService private readonly fileService: IFileService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IFileDialogService private readonly fileDialogService: IFileDialogService, + @ILifecycleService lifecycleService: ILifecycleService, + @ILabelService labelService: ILabelService, + @ILogService logService: ILogService, @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IDialogService private readonly dialogService: IDialogService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @ITextFileService textFileService: ITextFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @INotificationService notificationService: INotificationService, + @IWorkingCopyEditorService workingCopyEditorService: IWorkingCopyEditorService, + @IEditorService editorService: IEditorService, + @IElevatedFileService elevatedFileService: IElevatedFileService, + @IPathService private readonly pathService: IPathService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IPathService private readonly pathService: IPathService + @IDialogService private readonly dialogService: IDialogService ) { super(); // File manager - this.files = this._register(this.instantiationService.createInstance(FileWorkingCopyManager, this.workingCopyTypeId, this.fileModelFactory)) as unknown as IFileWorkingCopyManager; + this.files = this._register(new FileWorkingCopyManager( + this.workingCopyTypeId, + this.fileModelFactory, + fileService, lifecycleService, labelService, logService, workingCopyFileService, + workingCopyBackupService, uriIdentityService, fileDialogService, textFileService, filesConfigurationService, + workingCopyService, notificationService, workingCopyEditorService, editorService, elevatedFileService + )); // Untitled manager - this.untitled = this._register(this.instantiationService.createInstance(UntitledFileWorkingCopyManager, this.workingCopyTypeId, this.untitledFileModelFactory, async (workingCopy, options) => { - const result = await this.saveAs(workingCopy.resource, undefined, options); + this.untitled = this._register(new UntitledFileWorkingCopyManager( + this.workingCopyTypeId, + this.untitledFileModelFactory, + async (workingCopy, options) => { + const result = await this.saveAs(workingCopy.resource, undefined, options); - return result ? true : false; - })) as unknown as IUntitledFileWorkingCopyManager; + return result ? true : false; + }, + fileService, labelService, logService, workingCopyBackupService, workingCopyService + )); // Events this.onDidCreate = Event.any>(this.files.onDidCreate, this.untitled.onDidCreate); diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 74f77fc9b1c..c41f65dc8c0 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -66,12 +66,12 @@ export interface IResolvedUntitledFileWorkingCopy { /** * A delegate to enable saving of untitled file working copies. */ - (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; + (workingCopy: IUntitledFileWorkingCopy, options?: ISaveOptions): Promise; } export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { @@ -104,7 +104,7 @@ export class UntitledFileWorkingCopy ex readonly hasAssociatedFilePath: boolean, private readonly initialValue: VSBufferReadableStream | undefined, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, - private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @ILogService private readonly logService: ILogService diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index 53f8202eee9..8ca960de898 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, IUntitledFileWorkingCopySaveDelegate, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { Event, Emitter } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; @@ -114,12 +114,12 @@ export class UntitledFileWorkingCopyManager, - private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, + private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @IFileService fileService: IFileService, @ILabelService private readonly labelService: ILabelService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @ILogService logService: ILogService, - @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService ) { super(fileService, logService, workingCopyBackupService); } @@ -190,16 +190,18 @@ export class UntitledFileWorkingCopyManager; + this.saveDelegate, + this.workingCopyService, + this.workingCopyBackupService, + this.logService + ); // Register this.registerWorkingCopy(workingCopy); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index a0a4199ff0b..7512a46504e 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -29,15 +29,10 @@ suite('FileWorkingCopyManager', () => { manager = new FileWorkingCopyManager( 'testFileWorkingCopyType', new TestFileWorkingCopyModelFactory(), - accessor.fileService, - accessor.lifecycleService, - accessor.labelService, - instantiationService, - accessor.logService, - accessor.workingCopyFileService, - accessor.workingCopyBackupService, - accessor.uriIdentityService, - accessor.fileDialogService + accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, + accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, + accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, + accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService ); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts index d7e15bd7666..3c25fc4ae3e 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts @@ -33,14 +33,11 @@ suite('FileWorkingCopyManager2', () => { 'testFileWorkingCopyType2', new TestFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), - accessor.fileService, - instantiationService, - accessor.fileDialogService, - accessor.workingCopyFileService, - accessor.uriIdentityService, - accessor.dialogService, - accessor.environmentService, - accessor.pathService + accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, + accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, + accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, + accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, + accessor.environmentService, accessor.dialogService ); }); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 2b74144b657..7032a0e2074 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -32,14 +32,11 @@ suite('UntitledFileWorkingCopyManager', () => { 'testFileWorkingCopyTypeUntitled', new TestFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), - accessor.fileService, - instantiationService, - accessor.fileDialogService, - accessor.workingCopyFileService, - accessor.uriIdentityService, - accessor.dialogService, - accessor.environmentService, - accessor.pathService + accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, + accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, + accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, + accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService, accessor.pathService, + accessor.environmentService, accessor.dialogService ); }); From 224b0f229226e797f6d147adeac82b97473478fe Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 May 2021 06:05:58 -0700 Subject: [PATCH 194/270] Fix category and max-memory option args Fixes #124255 --- src/vs/platform/environment/node/argv.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 779b2e1286c..647829f6159 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -54,7 +54,7 @@ export const OPTIONS: OptionDescriptions> = { 'builtin-extensions-dir': { type: 'string' }, 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") }, - 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions.") }, + 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions."), args: 'category' }, 'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") }, 'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") }, 'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, @@ -74,7 +74,7 @@ export const OPTIONS: OptionDescriptions> = { 'inspect-extensions': { type: 'string', deprecates: 'debugPluginHost', args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") }, 'inspect-brk-extensions': { type: 'string', deprecates: 'debugBrkPluginHost', args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") }, 'disable-gpu': { type: 'boolean', cat: 't', description: localize('disableGPU', "Disable GPU hardware acceleration.") }, - 'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes).") }, + 'max-memory': { type: 'string', cat: 't', description: localize('maxMemory', "Max memory size for a window (in Mbytes)."), args: 'memory' }, 'telemetry': { type: 'boolean', cat: 't', description: localize('telemetry', "Shows all telemetry events which VS code collects.") }, 'remote': { type: 'string' }, From 1718a6e2ac8a884b9cace649ffff1a187937d9ba Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 May 2021 06:13:22 -0700 Subject: [PATCH 195/270] Hide hover on target element(s) click Fixes #124253 --- .../services/hover/browser/hoverService.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 03039126b11..f89f70cd98e 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -12,7 +12,8 @@ import { IContextMenuService, IContextViewService } from 'vs/platform/contextvie import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { HoverWidget } from 'vs/workbench/services/hover/browser/hoverWidget'; import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; export class HoverService implements IHoverService { declare readonly _serviceBrand: undefined; @@ -33,17 +34,28 @@ export class HoverService implements IHoverService { } this._currentHoverOptions = options; + const hoverDisposables = new DisposableStore(); const hover = this._instantiationService.createInstance(HoverWidget, options); - hover.onDispose(() => this._currentHoverOptions = undefined); + hover.onDispose(() => { + this._currentHoverOptions = undefined; + hoverDisposables.dispose(); + }); const provider = this._contextViewService as IContextViewProvider; provider.showContextView(new HoverContextViewDelegate(hover, focus)); hover.onRequestLayout(() => provider.layout()); + if ('targetElements' in options.target) { + for (const element of options.target.targetElements) { + hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this.hideHover())); + } + } else { + hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this.hideHover())); + } if ('IntersectionObserver' in window) { const observer = new IntersectionObserver(e => this._intersectionChange(e, hover), { threshold: 0 }); const firstTargetElement = 'targetElements' in options.target ? options.target.targetElements[0] : options.target; observer.observe(firstTargetElement); - hover.onDispose(() => observer.disconnect()); + hoverDisposables.add(toDisposable(() => observer.disconnect())); } return hover; From 713d58614049ec5870a8e50b415255413470f677 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 20 May 2021 15:13:34 +0200 Subject: [PATCH 196/270] :lipstick: less casts --- src/vs/base/test/common/stream.test.ts | 2 +- src/vs/platform/protocol/electron-main/protocolMainService.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index 4cb309a84ea..913cf6e0817 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -91,7 +91,7 @@ suite('Stream', () => { }); test('WriteableStream - end with error works', async () => { - const reducer = (errors: Error[]) => errors.length > 0 ? errors[0] : null as unknown as Error; + const reducer = (errors: Error[]) => errors[0]; const stream = newWriteableStream(reducer); stream.end(new Error('error')); diff --git a/src/vs/platform/protocol/electron-main/protocolMainService.ts b/src/vs/platform/protocol/electron-main/protocolMainService.ts index 4215275cf05..bd1eec7f933 100644 --- a/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -47,10 +47,10 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ const { defaultSession } = session; // Register vscode-file:// handler - defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback)); + defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback)); // Intercept any file:// access - defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback as unknown as ProtocolCallback)); + defaultSession.protocol.interceptFileProtocol(Schemas.file, (request, callback) => this.handleFileRequest(request, callback)); // Cleanup this._register(toDisposable(() => { From 8a6cc9cba7cdc56d2274d1ac5ef3b82a5d44a89a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 May 2021 07:18:50 -0700 Subject: [PATCH 197/270] Fix dynamic title for reconnected terminals Fixes #123783 --- .../contrib/terminal/browser/terminalInstance.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index e27b90e029e..8c1d17535bf 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -186,7 +186,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get navigationMode(): INavigationMode | undefined { return this._navigationModeAddon; } get isDisconnected(): boolean { return this._processManager.isDisconnected; } get isRemote(): boolean { return this._processManager.remoteAuthority !== undefined; } - get title(): string { return this._getTitle(); } + get title(): string { return this._title; } get icon(): ThemeIcon | undefined { return this._getIcon(); } get color(): string | undefined { return this._getColor(); } @@ -346,13 +346,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return undefined; } - private _getTitle(): string { - if (this.shellLaunchConfig.attachPersistentProcess?.title) { - return this.shellLaunchConfig.attachPersistentProcess.title; - } - return this._title; - } - addDisposable(disposable: IDisposable): void { this._register(disposable); } From 76154be4d4bc5768385261d8b0571868a3316c92 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 May 2021 07:34:27 -0700 Subject: [PATCH 198/270] Truncate Windows path from sequence, do title handling based on remote OS Fixes #124032 --- .../terminal/browser/terminalInstance.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 8c1d17535bf..52a505f1d4d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1598,10 +1598,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } switch (eventSource) { case TitleEventSource.Process: - if (isWindows) { - // Remove the .exe extension - title = path.basename(title); - title = title.split('.exe')[0]; + + if (this._processManager.os === OperatingSystem.Windows) { + // Extract the file name without extension + title = path.win32.parse(title).name; } else { const firstSpaceIndex = title.indexOf(' '); if (title.startsWith('/')) { @@ -1617,6 +1617,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { dispose(this._messageTitleDisposable); this._messageTitleDisposable = undefined; break; + case TitleEventSource.Sequence: + // On Windows, some shells will fire this with the full path which we want to trim + // to show just the file name. This should only happen if the title looks like an + // absolute Windows file path + title = 'C:\\blah blah\\pwsh.exe'; + if (this._processManager.os === OperatingSystem.Windows && title.match(/^[a-zA-Z]:\\.+\.[a-zA-Z]{1,3}/)) { + title = path.win32.parse(title).name; + } + break; } const didTitleChange = title !== this._title; this._title = title; From 875ada9bfad0771865181693cf4dab7b62e31935 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 May 2021 07:46:48 -0700 Subject: [PATCH 199/270] experimentalUseTitleEvent -> titleMode Fixes #22325 --- src/vs/platform/terminal/common/terminal.ts | 2 +- .../contrib/terminal/browser/terminalInstance.ts | 3 +-- .../workbench/contrib/terminal/common/terminal.ts | 2 +- .../terminal/common/terminalConfiguration.ts | 13 +++++++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 282aa660ced..d5407e7af06 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -69,7 +69,7 @@ export const enum TerminalSettingId { SplitCwd = 'terminal.integrated.splitCwd', WindowsEnableConpty = 'terminal.integrated.windowsEnableConpty', WordSeparators = 'terminal.integrated.wordSeparators', - ExperimentalUseTitleEvent = 'terminal.integrated.experimentalUseTitleEvent', + TitleMode = 'terminal.integrated.titleMode', EnableFileLinks = 'terminal.integrated.enableFileLinks', UnicodeVersion = 'terminal.integrated.unicodeVersion', ExperimentalLinkProvider = 'terminal.integrated.experimentalLinkProvider', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 52a505f1d4d..ec26541a6c5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1046,7 +1046,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } else { // Only listen for process title changes when a name is not provided - if (this._configHelper.config.experimentalUseTitleEvent) { + if (this._configHelper.config.titleMode === 'sequence') { // Set the title to the first event if the sequence hasn't set it yet Event.once(this._processManager.onProcessTitle)(e => { if (!this._title) { @@ -1621,7 +1621,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // On Windows, some shells will fire this with the full path which we want to trim // to show just the file name. This should only happen if the title looks like an // absolute Windows file path - title = 'C:\\blah blah\\pwsh.exe'; if (this._processManager.os === OperatingSystem.Windows && title.match(/^[a-zA-Z]:\\.+\.[a-zA-Z]{1,3}/)) { title = path.win32.parse(title).name; } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 9386ac00020..6fb4a49568c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -188,7 +188,7 @@ export interface ITerminalConfiguration { splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; windowsEnableConpty: boolean; wordSeparators: string; - experimentalUseTitleEvent: boolean; + titleMode: 'executable' | 'sequence'; enableFileLinks: boolean; unicodeVersion: '6' | '11'; experimentalLinkProvider: boolean; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 22e6521dafd..a9d58c95824 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -333,10 +333,15 @@ const terminalConfiguration: IConfigurationNode = { type: 'string', default: ' ()[]{}\',"`─' }, - [TerminalSettingId.ExperimentalUseTitleEvent]: { - description: localize('terminal.integrated.experimentalUseTitleEvent', "An experimental setting that will use the terminal title event for the dropdown title. This setting will only apply to new terminals."), - type: 'boolean', - default: false + [TerminalSettingId.TitleMode]: { + description: localize('terminal.integrated.titleMode', "Determines how the terminal's title is set, this shows up in the terminal's tab or dropdown entry."), + type: 'string', + enum: ['executable', 'sequence'], + markdownEnumDescriptions: [ + localize('titleMode.executable', "The title is set by the _terminal_, the name of the detected foreground process will be used."), + localize('titleMode.sequence', "The title is set by the _process_ via an escape sequence, this is useful if your shell dynamically sets the title.") + ], + default: 'executable' }, [TerminalSettingId.EnableFileLinks]: { description: localize('terminal.integrated.enableFileLinks', "Whether to enable file links in the terminal. Links can be slow when working on a network drive in particular because each file link is verified against the file system. Changing this will take effect only in new terminals."), From 62bbbcc7bf4c29904c40dc965f72e2950cd6ba15 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 May 2021 07:51:29 -0700 Subject: [PATCH 200/270] Remove log --- src/vs/workbench/contrib/terminal/browser/terminalView.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 09055f9d4f6..380f7b43c08 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -298,7 +298,6 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = } const dndBackgroundColor = theme.getColor(TERMINAL_DRAG_AND_DROP_BACKGROUND) || theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND); - console.log('dndBackgroundColor', dndBackgroundColor); if (dndBackgroundColor) { collector.addRule(`.monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay { background-color: ${dndBackgroundColor.toString()}; }`); } From f75152cd815601c6b4a1156d20224856a7c8d572 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 20 May 2021 09:53:11 -0700 Subject: [PATCH 201/270] notebook: fix certain kernels/output not working --- .../browser/view/renderers/webviewPreloads.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 424b8bf972e..40f70ec98f8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -142,7 +142,12 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend const text = await loadScriptSource(url, originalUri); return { activate: () => { - return invokeSourceWithGlobals(text, kernelPreloadGlobals); + try { + return invokeSourceWithGlobals(text, { ...kernelPreloadGlobals, scriptUrl: url }); + } catch (e) { + console.error(e); + throw e; + } } }; }; @@ -637,6 +642,10 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend outputContainer.appendChild(outputNode); resizeObserver.observe(outputNode, outputId, true); + if (content.type === RenderOutputType.Html) { + domEval(outputNode); + } + const clientHeight = outputNode.clientHeight; const cps = document.defaultView!.getComputedStyle(outputNode); if (clientHeight !== 0 && cps.padding === '0px') { From 0b355ffc89b2020a941ce149f2704acf13a49afd Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 20 May 2021 09:53:11 -0700 Subject: [PATCH 202/270] notebook: fix certain kernels/output not working --- .../browser/view/renderers/webviewPreloads.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 35609e1368b..e4297607aac 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -152,7 +152,12 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend const text = await loadScriptSource(url, originalUri); return { activate: () => { - return invokeSourceWithGlobals(text, kernelPreloadGlobals); + try { + return invokeSourceWithGlobals(text, { ...kernelPreloadGlobals, scriptUrl: url }); + } catch (e) { + console.error(e); + throw e; + } } }; }; @@ -647,6 +652,10 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend outputContainer.appendChild(outputNode); resizeObserver.observe(outputNode, outputId, true); + if (content.type === RenderOutputType.Html) { + domEval(outputNode); + } + const clientHeight = outputNode.clientHeight; const cps = document.defaultView!.getComputedStyle(outputNode); if (clientHeight !== 0 && cps.padding === '0px') { From 22f7f470b69e1570efc7497f4249d287c5ebd782 Mon Sep 17 00:00:00 2001 From: Andrea Mah Date: Thu, 20 May 2021 11:29:12 -0600 Subject: [PATCH 203/270] Apply Logan's suggested changes --- .../workbench/contrib/outline/browser/outlinePane.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 096e0a0d8e3..230d10664b6 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -38,6 +38,8 @@ import { Event } from 'vs/base/common/event'; import { ITreeSorter } from 'vs/base/browser/ui/tree/tree'; import { URI } from 'vs/base/common/uri'; import { EditorOverride } from 'vs/platform/editor/common/editor'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; const _ctxFollowsCursor = new RawContextKey('outlineFollowsCursor', false); const _ctxFilterOnType = new RawContextKey('outlineFiltersOnType', false); @@ -285,8 +287,13 @@ export class OutlinePane extends ViewPane { // feature: reveal outline selection in editor // on change -> reveal/select defining range - this._editorDisposables.add(tree.onDidOpen(e => newOutline.reveal(e.element, { ...e.editorOptions, override: EditorOverride.DISABLED }, e.sideBySide))); - + this._editorDisposables.add(tree.onDidOpen(e => { + let override: EditorOverride | string = EditorOverride.DISABLED; + if (this._editorService.activeEditor instanceof NotebookEditorInput || this._editorService.activeEditor instanceof CustomEditorInput) { + override = this._editorService.activeEditor.viewType; + } + newOutline.reveal(e.element, { ...e.editorOptions, override }, e.sideBySide); + })); // feature: reveal editor selection in outline const revealActiveElement = () => { if (!this._outlineViewState.followCursor || !newOutline.activeElement) { From 2270c36cff93861f7e876fbdd1fd3ce8b305712f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 20 May 2021 10:32:36 -0700 Subject: [PATCH 204/270] Use extension location instead of remote for asWebviewUri --- src/vs/workbench/api/common/extHostCodeInsets.ts | 6 +++++- src/vs/workbench/api/common/extHostNotebookKernels.ts | 6 +++++- src/vs/workbench/api/common/extHostWebview.ts | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 1ee7661d9fd..64650ee5ccf 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -12,6 +12,7 @@ import type * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import { generateUuid } from 'vs/base/common/uuid'; +import { Schemas } from 'vs/base/common/network'; export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { @@ -66,7 +67,10 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { private _options: vscode.WebviewOptions = Object.create(null); asWebviewUri(resource: vscode.Uri): vscode.Uri { - return asWebviewUri(this._uuid, resource, that._initData.remote.authority); + const remoteAuthority = extension.extensionLocation.scheme === Schemas.vscodeRemote + ? that._initData.remote.authority + : undefined; + return asWebviewUri(this._uuid, resource, remoteAuthority); } get cspSource(): string { diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index cc0be03859d..322707160f4 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -20,6 +20,7 @@ import { CellEditType, IImmediateCellEditOperation, NotebookCellExecutionState, import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { asArray } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; +import { Schemas } from 'vs/base/common/network'; interface IKernelData { extensionId: ExtensionIdentifier, @@ -190,7 +191,10 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); }, asWebviewUri(uri: URI) { - return asWebviewUri(String(handle), uri, that._initData.remote.authority); + const remoteAuthority = extension.extensionLocation.scheme === Schemas.vscodeRemote + ? that._initData.remote.authority + : undefined; + return asWebviewUri(String(handle), uri, remoteAuthority); }, // --- priority updateNotebookAffinity(notebook, priority) { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index b7bbde06834..ce4ce39d8cb 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -5,6 +5,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; @@ -69,7 +70,10 @@ export class ExtHostWebview implements vscode.Webview { public asWebviewUri(resource: vscode.Uri): vscode.Uri { this.#hasCalledAsWebviewUri = true; - return asWebviewUri(this.#handle, resource, this.#initData.remote.authority); + const remoteAuthority = this.#extension.extensionLocation.scheme === Schemas.vscodeRemote + ? this.#initData.remote.authority + : undefined; + return asWebviewUri(this.#handle, resource, remoteAuthority); } public get cspSource(): string { From 5b17052a5b3c9d2438bb1df85b2051587c57d49d Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 20 May 2021 10:53:36 -0700 Subject: [PATCH 205/270] Fix webview tests --- src/vs/workbench/test/browser/api/extHostWebview.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index f966a1ca426..bbdee0feefd 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -128,7 +129,13 @@ function createWebview(rpcProtocol: (IExtHostRpcService & IExtHostContext) | und const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); - const webview = extHostWebviewPanels.createWebviewPanel({} as IExtensionDescription, 'type', 'title', 1, {}); + const webview = extHostWebviewPanels.createWebviewPanel({ + extensionLocation: URI.from({ + scheme: remoteAuthority ? Schemas.vscodeRemote : Schemas.file, + authority: remoteAuthority, + path: '/ext/path', + }) + } as IExtensionDescription, 'type', 'title', 1, {}); return webview; } From 59abb887f6c0844e5ecd8b17c68981eeb79e9a2c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 20 May 2021 10:54:52 -0700 Subject: [PATCH 206/270] notebook: address messaging api changes --- src/vs/vscode.proposed.d.ts | 22 +++++++++++++------ .../browser/mainThreadNotebookRenderers.ts | 4 ++-- .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostNotebookEditor.ts | 4 ++++ .../api/common/extHostNotebookRenderers.ts | 19 +++++++++++----- .../notebookRendererMessagingServiceImpl.ts | 10 ++++----- .../notebookRendererMessagingService.ts | 2 +- 7 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 0fcaa4e05f2..f6c9685fcb5 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2107,6 +2107,18 @@ declare module 'vscode' { //#region @connor4312 - notebook messaging: https://github.com/microsoft/vscode/issues/123601 + export interface NotebookRendererMessage { + /** + * Editor that sent the message. + */ + editor: NotebookEditor; + + /** + * Message sent from the webview. + */ + message: T; + } + /** * Renderer messaging is used to communicate with a single renderer. It's * returned from {@link notebook.createRendererMessaging}. @@ -2115,18 +2127,14 @@ declare module 'vscode' { /** * Events that fires when a message is received from a renderer. */ - messageHandler?: (editor: NotebookEditor, message: TReceive) => void; + onDidReceiveMessage: Event>; /** * Sends a message to the renderer. + * @param editor Editor to target with the message * @param message Message to send */ - postMessage(message: TSend): void; - - /** - * Disposes of the messaging instance and unsubscribes from messages. - */ - dispose(): void; + postMessage(editor: NotebookEditor, message: TSend): void; } export namespace notebook { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts b/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts index 0883f85bc29..5adf89ce7a7 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts @@ -23,7 +23,7 @@ export class MainThreadNotebookRenderers extends Disposable implements MainThrea })); } - $postMessage(rendererId: string, message: unknown): void { - this.messaging.fireDidReceiveMessage(rendererId, message); + $postMessage(editorId: string, rendererId: string, message: unknown): void { + this.messaging.fireDidReceiveMessage(editorId, rendererId, message); } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e9473957ba6..56ab5feffc4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -915,7 +915,7 @@ export interface MainThreadNotebookKernelsShape extends IDisposable { } export interface MainThreadNotebookRenderersShape extends IDisposable { - $postMessage(rendererId: string, message: unknown): void; + $postMessage(editorId: string, rendererId: string, message: unknown): void; } export interface MainThreadUrlsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index 30f8c6d52e4..9804ba2be5b 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -73,6 +73,8 @@ class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { export class ExtHostNotebookEditor { + public static readonly apiEditorsToExtHost = new WeakMap(); + private _selections: vscode.NotebookRange[] = []; private _visibleRanges: vscode.NotebookRange[] = []; private _viewColumn?: vscode.ViewColumn; @@ -127,6 +129,8 @@ export class ExtHostNotebookEditor { return that.setDecorations(decorationType, range); } }; + + ExtHostNotebookEditor.apiEditorsToExtHost.set(this._editor, this); } return this._editor; } diff --git a/src/vs/workbench/api/common/extHostNotebookRenderers.ts b/src/vs/workbench/api/common/extHostNotebookRenderers.ts index 0d175f5c561..fcf5eb9cfd5 100644 --- a/src/vs/workbench/api/common/extHostNotebookRenderers.ts +++ b/src/vs/workbench/api/common/extHostNotebookRenderers.ts @@ -6,10 +6,11 @@ import { Emitter } from 'vs/base/common/event'; import { ExtHostNotebookRenderersShape, IMainContext, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookEditor } from 'vs/workbench/api/common/extHostNotebookEditor'; import * as vscode from 'vscode'; export class ExtHostNotebookRenderers implements ExtHostNotebookRenderersShape { - private readonly _rendererMessageEmitters = new Map>(); + private readonly _rendererMessageEmitters = new Map>>(); private readonly proxy: MainThreadNotebookRenderersShape; constructor(mainContext: IMainContext, private readonly _extHostNotebook: ExtHostNotebookController) { @@ -22,15 +23,21 @@ export class ExtHostNotebookRenderers implements ExtHostNotebookRenderersShape { return; } - this._rendererMessageEmitters.get(rendererId)?.fire({ document: editor.apiEditor, data: message }); + this._rendererMessageEmitters.get(rendererId)?.fire({ editor: editor.apiEditor, message }); } public createRendererMessaging(rendererId: string): vscode.NotebookRendererMessaging { - const emitter = this.getOrCreateEmitterFor(rendererId); - const listener = emitter.event(e => messaging.messageHandler?.(e.document, e.data)); const messaging: vscode.NotebookRendererMessaging = { - dispose: () => listener.dispose(), - postMessage: message => this.proxy.$postMessage(rendererId, message), + onDidReceiveMessage: (...args) => + this.getOrCreateEmitterFor(rendererId).event(...args), + postMessage: (editor, message) => { + const extHostEditor = ExtHostNotebookEditor.apiEditorsToExtHost.get(editor); + if (!extHostEditor) { + throw new Error(`The first argument to postMessage() must be a NotebookEditor`); + } + + this.proxy.$postMessage(extHostEditor.id, rendererId, message); + }, }; return messaging; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts index 6524ebb4224..0d816f42fb5 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { INotebookRendererMessagingService, IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -16,7 +16,7 @@ export class NotebookRendererMessagingService implements INotebookRendererMessag * be sent once activation finishes, or undefined if activation is complete. */ private readonly activations = new Map(); - private readonly receiveMessageEmitter = new Emitter<{ rendererId: string, message: unknown }>(); + private readonly receiveMessageEmitter = new Emitter<{ editorId: string; rendererId: string, message: unknown }>(); public readonly onDidReceiveMessage = this.receiveMessageEmitter.event; private readonly postMessageEmitter = new Emitter(); public readonly onShouldPostMessage = this.postMessageEmitter.event; @@ -24,8 +24,8 @@ export class NotebookRendererMessagingService implements INotebookRendererMessag constructor(@IExtensionService private readonly extensionService: IExtensionService) { } /** @inheritdoc */ - public fireDidReceiveMessage(rendererId: string, message: unknown): void { - this.receiveMessageEmitter.fire({ rendererId, message }); + public fireDidReceiveMessage(editorId: string, rendererId: string, message: unknown): void { + this.receiveMessageEmitter.fire({ editorId, rendererId, message }); } /** @inheritdoc */ @@ -49,7 +49,7 @@ export class NotebookRendererMessagingService implements INotebookRendererMessag /** @inheritdoc */ public getScoped(editorId: string): IScopedRendererMessaging { return { - onDidReceiveMessage: this.onDidReceiveMessage, + onDidReceiveMessage: Event.filter(this.onDidReceiveMessage, e => e.editorId === editorId), postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message), }; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts b/src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts index 0f00b8f6af3..2264baa168f 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookRendererMessagingService.ts @@ -28,7 +28,7 @@ export interface INotebookRendererMessagingService { /** * Called when the main thread gets a message for a renderer. */ - fireDidReceiveMessage(rendererId: string, message: unknown): void; + fireDidReceiveMessage(editorId: string, rendererId: string, message: unknown): void; } export interface IScopedRendererMessaging { From bd934d89ea0b457e12448fef71217ef766f87a1a Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 20 May 2021 14:00:35 -0400 Subject: [PATCH 207/270] Use untyped editors for workspace trust --- .../services/editor/browser/editorService.ts | 70 ++++++++++++++++--- .../services/editor/common/editorService.ts | 5 ++ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 41957dba74a..d50c4165325 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -17,7 +17,7 @@ import { URI } from 'vs/base/common/uri'; import { basename, joinPath, isEqual } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; +import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorOverrideEntry, SidebySideURI } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { coalesce, distinct, firstOrDefault, insert } from 'vs/base/common/arrays'; @@ -793,11 +793,63 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#endregion //#region Workspace Trust + + /** + * Given an array of typed or untyped editor inputs converts them to an array of resources + * @param editors The editor inputs + * @returns The array of resources + */ + private convertToResourceArray(editors: Array): Array { + const resources: Array = []; + editors.forEach(editor => { + if (isEditorInputWithOptions(editor)) { + if (editor.editor instanceof SideBySideEditorInput) { + if (!editor.editor.primary.resource || !editor.editor.secondary.resource) { + return; + } + resources.push({ + primary: editor.editor.primary.resource, + secondary: editor.editor.secondary.resource + }); + } + } + const resourceDiffEditor = editor as IResourceDiffEditorInput; + if (resourceDiffEditor.leftResource && resourceDiffEditor.rightResource) { + resources.push({ + primary: resourceDiffEditor.leftResource, + secondary: resourceDiffEditor.rightResource + }); + } + const otherEditors = editor as IResourceEditorInput | IUntitledTextResourceEditorInput; + if (otherEditors.resource) { + resources.push(otherEditors.resource); + } + }); + return resources; + } + + private isSideBySideURI(obj: unknown): obj is SidebySideURI { + const sideBySideURI: SidebySideURI = obj as SidebySideURI; + if (sideBySideURI.primary && sideBySideURI.secondary) { + return true; + } + return false; + } /** * Given a set of resources returns a boolean to indicate whether or not to proceed with opening */ - private async handleWorkspaceTrust(resources: Array): Promise { - const filteredResources = coalesce(resources); + private async handleWorkspaceTrust(editors: Array): Promise { + const resources = this.convertToResourceArray(editors); + const filteredResources: URI[] = []; + // Convert the array into a flat resource array + resources.forEach(r => { + if (this.isSideBySideURI(r)) { + filteredResources.push(r.primary); + filteredResources.push(r.secondary); + } else { + filteredResources.push(r); + } + }); const trustResult = await this.workspaceTrustRequestService.requestOpenUris(filteredResources); if (trustResult === WorkspaceTrustUriResponse.OpenInNewWindow) { const filesToOpen: IFileToOpen[] = []; @@ -807,6 +859,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { } return trustResult === WorkspaceTrustUriResponse.Open; } + //#endregion //#region openEditors() @@ -814,6 +867,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise; async openEditors(editors: Array, group?: OpenInEditorGroup): Promise { + + if (!(await this.handleWorkspaceTrust(editors))) { + return []; + } + // Convert to typed editors and options const typedEditors: IEditorInputWithOptions[] = editors.map(editor => { if (isEditorInputWithOptions(editor)) { @@ -826,12 +884,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { }; }); - const editorResources = typedEditors.map(e => e.editor.resource); - if (!(await this.handleWorkspaceTrust(editorResources))) { - typedEditors.forEach(editor => editor.editor.dispose()); - return []; - } - // Find target groups to open const mapGroupToEditorsCandidates = new Map(); if (group === SIDE_GROUP) { diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index cd4337732d1..0f864f91702 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -16,6 +16,11 @@ export const IEditorService = createDecorator('editorService'); export type IResourceEditorInputType = IResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput; +export interface SidebySideURI { + primary: URI; + secondary: URI; +} + export interface IResourceEditorReplacement { readonly editor: IResourceEditorInputType; readonly replacement: IResourceEditorInputType; From 6b097212e69119c2b147ee8bf3939c9c04490808 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 20 May 2021 14:03:02 -0400 Subject: [PATCH 208/270] Revert "Revert "Closes #122433"" This reverts commit 29c61570a5b9a669f777bb28b5acd5c37d99edbe. --- .../editor/browser/editorOverrideService.ts | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 990a4acf6d9..45c897382db 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -40,9 +40,13 @@ type ContributionPoints = Array; export class EditorOverrideService extends Disposable implements IEditorOverrideService { readonly _serviceBrand: undefined; + // Constants private static readonly configureDefaultID = 'promptOpenWith.configureDefault'; - private _contributionPoints: Map = new Map(); private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; + private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; + + // Data Stores + private _contributionPoints: Map = new Map(); private cache: Set | undefined; constructor( @@ -129,8 +133,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } const input = await this.doOverrideEditorInput(editor, options, group, selectedContribution); if (conflictingDefault && input) { - // Wait one second to give the user ample time to see the current editor then ask them to configure a default - this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); + // Show the conflicting default dialog + await this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); } // Add the group as we might've changed it with the quickpick @@ -367,12 +371,23 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } private async doHandleConflictingDefaults(editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { - const makeCurrentEditorDefault = () => { - const viewType = currentEditor.viewType; - if (viewType) { - this.updateUserAssociations(`*${extname(currentEditor.resource!)}`, viewType); - } + type StoredChoice = { + [key: string]: string[]; }; + const contributionPoints = this.findMatchingContributions(currentEditor.resource!); + const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorOverrideService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); + const globForResource = `*${extname(currentEditor.resource!)}`; + // Writes to the storage service that a choice has been made for the currently installed editors + const writeCurrentEditorsToStorage = () => { + storedChoices[globForResource] = []; + contributionPoints.forEach(contrib => storedChoices[globForResource].push(contrib.editorInfo.id)); + this.storageService.store(EditorOverrideService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); + }; + + // If the user has already made a choice for this editor we don't want to ask them again + if (storedChoices[globForResource]?.find(editorID => editorID === currentEditor.viewType)) { + return; + } const handle = this.notificationService.prompt(Severity.Warning, localize('editorOverride.conflictingDefaults', 'There are multiple default editors available for the resource.'), @@ -400,12 +415,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }, { label: localize('editorOverride.keepDefault', 'Keep {0}', editorName), - run: makeCurrentEditorDefault + run: writeCurrentEditorsToStorage } ]); // If the user pressed X we assume they want to keep the current editor as default const onCloseListener = handle.onDidClose(() => { - makeCurrentEditorDefault(); + writeCurrentEditorsToStorage(); onCloseListener.dispose(); }); } From dae2a9d4ebcb2673ad21da0ab15b116558ba8653 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 20 May 2021 20:03:55 +0200 Subject: [PATCH 209/270] do not auto focus on ios extensions input and keybindings #122044 --- .../contrib/extensions/browser/extensionsViewlet.ts | 6 +++--- .../contrib/preferences/browser/keybindingsEditor.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index d90fe39e080..2a29f50fdb5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -56,7 +56,7 @@ import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { VirtualWorkspaceContext, WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { isWeb } from 'vs/base/common/platform'; +import { isIOS, isWeb } from 'vs/base/common/platform'; import { installLocalInRemoteIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; @@ -557,7 +557,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this)); this._register(this.onDidChangeVisibility(visible => { - if (visible) { + if (visible && !isIOS) { this.searchBox!.focus(); } })); @@ -609,7 +609,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } override focus(): void { - if (this.searchBox) { + if (this.searchBox && !isIOS) { this.searchBox.focus(); } } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 26c4b58ff4e..21f20c9720e 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/keybindingsEditor'; import { localize } from 'vs/nls'; import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; -import { OS } from 'vs/base/common/platform'; +import { isIOS, OS } from 'vs/base/common/platform'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { CheckboxActionViewItem } from 'vs/base/browser/ui/checkbox/checkbox'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; @@ -174,7 +174,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP const activeKeybindingEntry = this.activeKeybindingEntry; if (activeKeybindingEntry) { this.selectEntry(activeKeybindingEntry); - } else { + } else if (!isIOS) { this.searchWidget.focus(); } } From 6f2381e33d7450c79ba37f6803e3e6afd84dfcb6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 20 May 2021 13:17:33 -0500 Subject: [PATCH 210/270] Terminal tab icon API (#124004) --- src/vs/platform/terminal/common/terminal.ts | 20 +++-- .../terminal/common/terminalProcess.ts | 4 +- .../platform/terminal/node/ptyHostService.ts | 4 +- src/vs/platform/terminal/node/ptyService.ts | 11 +-- .../terminal/node/terminalProfiles.ts | 28 ++++--- src/vs/platform/theme/common/themeService.ts | 4 + src/vs/vscode.proposed.d.ts | 11 ++- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 4 +- .../api/common/extHostTerminalService.ts | 9 +- .../api/node/extHostTerminalService.ts | 2 +- .../tasks/browser/terminalTaskSystem.ts | 2 +- .../terminal/browser/media/terminal.css | 19 +++++ .../terminal/browser/remoteTerminalService.ts | 4 +- .../contrib/terminal/browser/terminal.ts | 5 +- .../contrib/terminal/browser/terminalIcon.ts | 53 ++++++++++++ .../terminal/browser/terminalInstance.ts | 25 +++--- .../browser/terminalProfileResolverService.ts | 63 ++++++++++---- .../terminal/browser/terminalQuickAccess.ts | 18 +++- .../terminal/browser/terminalService.ts | 4 +- .../terminal/browser/terminalTabsList.ts | 37 +++++---- .../contrib/terminal/browser/terminalView.ts | 83 +++++++++++++++++-- .../terminal/common/remoteTerminalChannel.ts | 4 +- .../contrib/terminal/common/terminal.ts | 3 +- .../electron-sandbox/localTerminalService.ts | 3 +- .../terminalProfileResolverService.ts | 2 +- .../test/browser/workbenchTestServices.ts | 2 +- 27 files changed, 319 insertions(+), 107 deletions(-) create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalIcon.ts diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index d5407e7af06..3c9c10db0d3 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -8,6 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export const enum TerminalSettingId { ShellLinux = 'terminal.integrated.shell.linux', @@ -116,7 +117,7 @@ export interface IPtyHostAttachTarget { workspaceId: string; workspaceName: string; isOrphan: boolean; - icon: string | undefined; + icon: TerminalIcon | undefined; } export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo; @@ -174,7 +175,7 @@ export interface IOffProcessTerminalService { getShellEnvironment(): Promise; setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise; updateTitle(id: number, title: string): Promise; - updateIcon(id: number, icon: string, color?: string): Promise; + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise; getTerminalLayoutInfo(): Promise; reduceConnectionGraceTime(): Promise; } @@ -240,7 +241,7 @@ export interface IPtyService { /** Confirm the process is _not_ an orphan. */ orphanQuestionReply(id: number): Promise; updateTitle(id: number, title: string): Promise; - updateIcon(id: number, icon: string, color?: string): Promise; + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise; getDefaultSystemShell(osOverride?: OperatingSystem): Promise; getProfiles?(includeDetectedProfiles?: boolean): Promise; getEnvironment(): Promise; @@ -348,7 +349,7 @@ export interface IShellLaunchConfig { /** * This is a terminal that attaches to an already running terminal. */ - attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; icon?: string; color?: string }; + attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; icon?: TerminalIcon; color?: string }; /** * Whether the terminal process environment should be exactly as provided in @@ -388,10 +389,9 @@ export interface IShellLaunchConfig { isExtensionOwnedTerminal?: boolean; /** - * The codicon ID to use for this terminal. If not specified it will use the default fallback - * icon. + * The icon for the terminal, used primarily in the terminal tab. */ - icon?: string; + icon?: TerminalIcon; /** * The color ID to use for this terminal. If not specified it will use the default fallback @@ -399,6 +399,8 @@ export interface IShellLaunchConfig { color?: string; } +export type TerminalIcon = ThemeIcon | URI | { light: URI; dark: URI }; + export interface IShellLaunchConfigDto { name?: string; executable?: string; @@ -549,7 +551,7 @@ export interface ITerminalProfile { args?: string | string[] | undefined; env?: ITerminalEnvironment; overrideName?: boolean; - icon?: string; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; } export interface ITerminalDimensionsOverride extends Readonly { @@ -570,7 +572,7 @@ export interface IBaseUnresolvedTerminalProfile { args?: string | string[] | undefined; isAutoDetected?: boolean; overrideName?: boolean; - icon?: string; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; env?: ITerminalEnvironment; } diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts index 3fd0e6bb77b..914e85c8a94 100644 --- a/src/vs/platform/terminal/common/terminalProcess.ts +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { UriComponents } from 'vs/base/common/uri'; -import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; export interface ISingleTerminalConfiguration { @@ -55,7 +55,7 @@ export interface IProcessDetails { workspaceId: string; workspaceName: string; isOrphan: boolean; - icon: string | undefined; + icon: TerminalIcon | undefined; color: string | undefined; } diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index b57f55bc25e..b9ad90e8602 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -5,7 +5,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType, ITerminalProfile, IRequestResolveVariablesEvent, SafeConfigProvider, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType, ITerminalProfile, IRequestResolveVariablesEvent, SafeConfigProvider, TerminalSettingId, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { FileAccess } from 'vs/base/common/network'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; @@ -170,7 +170,7 @@ export class PtyHostService extends Disposable implements IPtyService { updateTitle(id: number, title: string): Promise { return this._proxy.updateTitle(id, title); } - updateIcon(id: number, icon: string, color?: string): Promise { + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise { return this._proxy.updateIcon(id, icon, color); } attachToProcess(id: number): Promise { diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 1d06ff418eb..950d09a069a 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -5,7 +5,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType, IProcessReadyEvent, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; @@ -17,6 +17,7 @@ import { getSystemShell } from 'vs/base/node/shell'; import { getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; import { execFile } from 'child_process'; import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; +import { URI } from 'vs/base/common/uri'; type WorkspaceId = string; @@ -118,7 +119,7 @@ export class PtyService extends Disposable implements IPtyService { this._throwIfNoPty(id).setTitle(title); } - async updateIcon(id: number, icon: string, color?: string): Promise { + async updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { this._throwIfNoPty(id).setIcon(icon, color); } @@ -303,14 +304,14 @@ export class PersistentTerminalProcess extends Disposable { get pid(): number { return this._pid; } get title(): string { return this._title || this._terminalProcess.currentTitle; } - get icon(): string | undefined { return this._icon; } + get icon(): TerminalIcon | undefined { return this._icon; } get color(): string | undefined { return this._color; } setTitle(title: string): void { this._title = title; } - setIcon(icon: string, color?: string): void { + setIcon(icon: TerminalIcon, color?: string): void { this._icon = icon; this._color = color; } @@ -323,7 +324,7 @@ export class PersistentTerminalProcess extends Disposable { readonly shouldPersistTerminal: boolean, cols: number, rows: number, private readonly _logService: ILogService, - private _icon?: string, + private _icon?: TerminalIcon, private _color?: string ) { super(); diff --git a/src/vs/platform/terminal/node/terminalProfiles.ts b/src/vs/platform/terminal/node/terminalProfiles.ts index 150b2dca5bf..f2777cd794b 100644 --- a/src/vs/platform/terminal/node/terminalProfiles.ts +++ b/src/vs/platform/terminal/node/terminalProfiles.ts @@ -13,6 +13,8 @@ import * as pfs from 'vs/base/node/pfs'; import { ITerminalEnvironment, ITerminalProfile, ITerminalProfileObject, ProfileSource, SafeConfigProvider, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { Codicon } from 'vs/base/common/codicons'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { URI } from 'vs/base/common/uri'; let profileSources: Map | undefined; @@ -80,12 +82,12 @@ async function detectAvailableWindowsProfiles( if (includeDetectedProfiles) { detectedProfiles.set('PowerShell', { source: ProfileSource.Pwsh, - icon: Codicon.terminalPowershell.id, + icon: Codicon.terminalPowershell, isAutoDetected: true }); detectedProfiles.set('Windows PowerShell', { path: `${system32Path}\\WindowsPowerShell\\v1.0\\powershell.exe`, - icon: Codicon.terminalPowershell.id, + icon: Codicon.terminalPowershell, isAutoDetected: true }); detectedProfiles.set('Git Bash', { @@ -102,7 +104,7 @@ async function detectAvailableWindowsProfiles( }); detectedProfiles.set('Command Prompt', { path: `${system32Path}\\cmd.exe`, - icon: Codicon.terminalCmd.id, + icon: Codicon.terminalCmd, isAutoDetected: true }); } @@ -137,7 +139,7 @@ async function transformToTerminalProfiles( if (profile === null) { continue; } let originalPaths: string[]; let args: string[] | string | undefined; - let icon: string | undefined; + let icon: ThemeIcon | URI | { light: URI, dark: URI } | undefined = undefined; if ('source' in profile) { const source = profileSources?.get(profile.source); if (!source) { @@ -147,11 +149,15 @@ async function transformToTerminalProfiles( // if there are configured args, override the default ones args = profile.args || source.args; - icon = profile.icon || source.icon; + if (profile.icon) { + icon = profile.icon; + } else if (source.icon) { + icon = source.icon; + } } else { originalPaths = Array.isArray(profile.path) ? profile.path : [profile.path]; args = isWindows ? profile.args : Array.isArray(profile.args) ? profile.args : undefined; - icon = profile.icon; + icon = profile.icon || undefined; } const paths = (await variableResolver?.(originalPaths)) || originalPaths.slice(); @@ -190,7 +196,7 @@ async function initializeWindowsProfiles(): Promise { profileSources.set('PowerShell', { profileName: 'PowerShell', paths: await getPowershellPaths(), - icon: 'terminal-powershell' + icon: ThemeIcon.asThemeIcon(Codicon.terminalPowershell) }); } @@ -240,12 +246,12 @@ async function getWslProfiles(wslPath: string, defaultProfileName: string | unde isDefault: profileName === defaultProfileName }; if (distroName.includes('Ubuntu')) { - profile.icon = 'terminal-ubuntu'; + profile.icon = ThemeIcon.asThemeIcon(Codicon.terminalUbuntu); } else if (distroName.includes('Debian')) { - profile.icon = 'terminal-debian'; + profile.icon = ThemeIcon.asThemeIcon(Codicon.terminalDebian); } else { - profile.icon = 'terminal-linux'; + profile.icon = ThemeIcon.asThemeIcon(Codicon.terminalLinux); } // Add the profile @@ -343,5 +349,5 @@ interface IPotentialTerminalProfile { profileName: string; paths: string[]; args?: string[]; - icon?: string; + icon?: ThemeIcon | URI | { light: URI, dark: URI }; } diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index 4032bab91fc..9867c464ae3 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -67,6 +67,10 @@ export namespace ThemeIcon { return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id; } + export function asThemeIcon(codicon: Codicon): ThemeIcon { + return { id: codicon.id }; + } + export const asClassNameArray: (icon: ThemeIcon) => string[] = CSSIcon.asClassNameArray; export const asClassName: (icon: ThemeIcon) => string = CSSIcon.asClassName; export const asCSSSelector: (icon: ThemeIcon) => string = CSSIcon.asCSSSelector; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b2985b4990f..138eb225334 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -891,9 +891,16 @@ declare module 'vscode' { export interface TerminalOptions { /** - * A codicon ID to associate with this terminal. + * The icon path or {@link ThemeIcon} for the terminal. */ - readonly icon?: string; + readonly iconPath?: Uri | { light: Uri; dark: Uri } | { id: string, color?: { id: string } }; + } + + export interface ExtensionTerminalOptions { + /** + * A themeIcon, Uri, or light and dark Uris to use as the terminal tab icon + */ + readonly iconPath?: Uri | { light: Uri; dark: Uri } | { id: string, color?: { id: string } }; } //#endregion diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9bde67e9b9f..094d3f2ae32 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -639,7 +639,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if ('pty' in nameOrOptions) { return extHostTerminalService.createExtensionTerminal(nameOrOptions); } - if (nameOrOptions.icon) { + if (nameOrOptions.iconPath) { checkProposedApiEnabled(extension); } return extHostTerminalService.createTerminalFromOptions(nameOrOptions); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4cb3d5e300d..f9068ca9931 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -40,7 +40,7 @@ import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPro import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile } from 'vs/platform/terminal/common/terminal'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; @@ -454,7 +454,7 @@ export interface TerminalLaunchConfig { shellArgs?: string[] | string; cwd?: string | UriComponents; env?: ITerminalEnvironment; - icon?: string; + icon?: URI | { light: URI; dark: URI } | ThemeIcon; initialText?: string; waitOnExit?: boolean; strictEnv?: boolean; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 25b677b7041..be6ce95e65f 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -20,6 +20,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -116,7 +117,7 @@ export class ExtHostTerminal { shellArgs?: string[] | string, cwd?: string | URI, env?: ITerminalEnvironment, - icon?: string, + icon?: URI | { light: URI; dark: URI } | ThemeIcon, initialText?: string, waitOnExit?: boolean, strictEnv?: boolean, @@ -130,11 +131,11 @@ export class ExtHostTerminal { await this._proxy.$createTerminal(this._id, { name: this._name, shellPath, shellArgs, cwd, env, icon, initialText, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal, isExtensionOwnedTerminal }); } - public async createExtensionTerminal(): Promise { + public async createExtensionTerminal(iconPath?: URI | { light: URI; dark: URI } | ThemeIcon): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } - await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionCustomPtyTerminal: true }); + await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionCustomPtyTerminal: true, icon: iconPath }); // At this point, the id has been set via `$acceptTerminalOpened` if (typeof this._id === 'string') { throw new Error('Terminal creation failed'); @@ -348,7 +349,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal().then(id => { + terminal.createExtensionTerminal(options.iconPath).then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 91043450d7e..8173c051ff2 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -32,7 +32,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { withNullAsUndefined(options.shellArgs), withNullAsUndefined(options.cwd), withNullAsUndefined(options.env), - withNullAsUndefined(options.icon), + withNullAsUndefined(options.iconPath), withNullAsUndefined(options.message), /*options.waitOnExit*/ undefined, withNullAsUndefined(options.strictEnv), diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 0ee3da84157..09697c78813 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1209,7 +1209,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } if (this.currentTask.shellLaunchConfig) { - this.currentTask.shellLaunchConfig.icon = 'tools'; + this.currentTask.shellLaunchConfig.icon = { id: 'tools' }; } let prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated; diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 1b1291a4c67..58cd1e99f25 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -290,6 +290,25 @@ color: inherit; } +.monaco-workbench .pane-body.integrated-terminal .tabs-container.has-text .tabs-list .terminal-tabs-entry .uri-icon { + background-repeat: no-repeat; + background-size: contain; + margin-right: 4px; + height: 100%; +} + +.monaco-workbench .terminal-uri-icon .monaco-highlighted-label .codicon, +.monaco-action-bar .terminal-uri-icon.single-terminal-tab.action-label .codicon { + background-size: 16px; +} +.monaco-workbench .terminal-uri-icon .monaco-highlighted-label .codicon::before, +.monaco-action-bar .terminal-uri-icon.single-terminal-tab.action-label:not(.alt-command) .codicon::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; +} + .monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay { display: block; position: absolute; diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index 9495b9f928f..3ed3747df3d 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { RemotePty } from 'vs/workbench/contrib/terminal/browser/remotePty'; import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -215,7 +215,7 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal await this._remoteTerminalChannel?.updateTitle(id, title); } - async updateIcon(id: number, icon: string, color?: string): Promise { + async updateIcon(id: number, icon: TerminalIcon, color?: string): Promise { await this._remoteTerminalChannel?.updateIcon(id, icon, color); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 632674fa598..6c2315fc798 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IOffProcessTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IOffProcessTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { ICommandTracker, INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; @@ -17,7 +17,6 @@ import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { ICompleteTerminalConfiguration } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalInstanceService = createDecorator('terminalInstanceService'); @@ -267,7 +266,7 @@ export interface ITerminalInstance { readonly rows: number; readonly maxCols: number; readonly maxRows: number; - readonly icon?: ThemeIcon; + readonly icon?: TerminalIcon; readonly color?: string; readonly statusList: ITerminalStatusList; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts b/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts new file mode 100644 index 00000000000..b4b18a7b9bd --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { hash } from 'vs/base/common/hash'; +import { URI } from 'vs/base/common/uri'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; + + +export function getColorClass(terminal: ITerminalInstance): string | undefined { + let color = undefined; + if (terminal.color) { + color = terminal.color; + } else if (ThemeIcon.isThemeIcon(terminal.icon) && terminal.icon.color) { + color = terminal.icon.color.id.replace('.', '_'); + } + if (color) { + return `terminal-icon-${color}`; + } + return undefined; +} + +export function getUriClasses(terminal: ITerminalInstance, colorScheme: ColorScheme): string[] | undefined { + const icon = terminal.icon; + if (!icon) { + return undefined; + } + const iconClasses: string[] = []; + let uri = undefined; + if (icon instanceof URI) { + uri = icon; + } else if (icon instanceof Object && 'light' in icon && 'dark' in icon) { + uri = colorScheme === ColorScheme.LIGHT ? icon.light : icon.dark; + } + if (uri instanceof URI) { + const uriIconKey = hash(uri.path).toString(36); + const className = `terminal-uri-icon-${uriIconKey}`; + iconClasses.push(className); + iconClasses.push(`terminal-uri-icon`); + } + return iconClasses; +} + +export function getIconId(terminal: ITerminalInstance): string { + if (!terminal.icon || (terminal.icon instanceof Object && !('id' in terminal.icon))) { + return Codicon.terminal.id; + } + return terminal.icon.id; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index ec26541a6c5..fb2acc2531f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -21,7 +21,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; -import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, DEFAULT_COMMANDS_TO_SKIP_SHELL, TERMINAL_CREATION_COMMANDS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, SUGGESTED_RENDERER_TYPE, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -46,7 +46,7 @@ import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTy import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { IProductService } from 'vs/platform/product/common/productService'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { AutoOpenBarrier } from 'vs/base/common/async'; @@ -187,7 +187,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get isDisconnected(): boolean { return this._processManager.isDisconnected; } get isRemote(): boolean { return this._processManager.remoteAuthority !== undefined; } get title(): string { return this._title; } - get icon(): ThemeIcon | undefined { return this._getIcon(); } + get icon(): TerminalIcon | undefined { return this._getIcon(); } get color(): string | undefined { return this._getColor(); } private readonly _onExit = new Emitter(); @@ -320,17 +320,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { })); } - private _getIcon(): Codicon | undefined { - if (this.shellLaunchConfig.icon) { - return iconRegistry.get(this.shellLaunchConfig.icon); + private _getIcon(): TerminalIcon | undefined { + const icon = this._shellLaunchConfig.icon || this._shellLaunchConfig.attachPersistentProcess?.icon; + if (!icon) { + return this._processManager.processState >= ProcessState.Launching ? Codicon.terminal : undefined; } - if (this.shellLaunchConfig?.attachPersistentProcess?.icon) { - return iconRegistry.get(this.shellLaunchConfig.attachPersistentProcess.icon); - } - if (this._processManager.processState >= ProcessState.Launching) { - return Codicon.terminal; - } - return undefined; + return icon; } private _getColor(): string | undefined { @@ -1804,8 +1799,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { title: nls.localize('changeTerminalIcon', "Change Icon"), matchOnDescription: true }); - if (result) { - this.shellLaunchConfig.icon = result.description; + if (result && result.description) { + this.shellLaunchConfig.icon = iconRegistry.get(result.description); this._onIconChanged.fire(this); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index 399cf6b2f63..71a5445705e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -13,12 +13,14 @@ import { IRemoteTerminalService, ITerminalService } from 'vs/workbench/contrib/t import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IProcessEnvironment, OperatingSystem, OS } from 'vs/base/common/platform'; -import { IShellLaunchConfig, ITerminalProfile, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalProfile, TerminalIcon, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IShellLaunchConfigResolveOptions, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import * as path from 'vs/base/common/path'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { debounce } from 'vs/base/common/decorators'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { URI, UriComponents } from 'vs/base/common/uri'; export interface IProfileContextProvider { getDefaultSystemShell: (remoteAuthority: string | undefined, os: OperatingSystem) => Promise; @@ -43,7 +45,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro private readonly _logService: ILogService, private readonly _terminalService: ITerminalService, private readonly _workspaceContextService: IWorkspaceContextService, - private readonly _remoteAgentService: IRemoteAgentService, + private readonly _remoteAgentService: IRemoteAgentService ) { if (this._remoteAgentService.getConnection()) { this._remoteAgentService.getEnvironment().then(env => this._primaryBackendOs = env?.os || OS); @@ -71,10 +73,14 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } resolveIcon(shellLaunchConfig: IShellLaunchConfig, os: OperatingSystem): void { - if (shellLaunchConfig.executable) { + if (shellLaunchConfig.icon) { + shellLaunchConfig.icon = this._getCustomIcon(shellLaunchConfig.icon) || Codicon.terminal; return; } + if (shellLaunchConfig.executable) { + return; + } const defaultProfile = this._getUnresolvedRealDefaultProfile(os); if (defaultProfile) { shellLaunchConfig.icon = defaultProfile.icon; @@ -106,8 +112,7 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro // Verify the icon is valid, and fallback correctly to the generic terminal id if there is // an issue - shellLaunchConfig.icon = this._verifyIcon(shellLaunchConfig.icon) || this._verifyIcon(resolvedProfile.icon) || Codicon.terminal.id; - + shellLaunchConfig.icon = this._getCustomIcon(shellLaunchConfig.icon) || this._getCustomIcon(resolvedProfile.icon) || Codicon.terminal; // Override the name if specified if (resolvedProfile.overrideName) { shellLaunchConfig.name = resolvedProfile.profileName; @@ -119,12 +124,6 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } } - private _verifyIcon(iconId?: string): string | undefined { - if (!iconId || !iconRegistry.get(iconId)) { - return undefined; - } - return iconId; - } async getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise { return (await this.getDefaultProfile(options)).path; @@ -142,6 +141,36 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro return this._context.getEnvironment(remoteAuthority); } + private _getCustomIcon(icon?: unknown): TerminalIcon | undefined { + if (!icon) { + return undefined; + } + if (typeof icon === 'string') { + return iconRegistry.get(icon); + } + if (ThemeIcon.isThemeIcon(icon)) { + return icon; + } + if (URI.isUri(icon) || this._isUriComponents(icon)) { + return URI.revive(icon); + } + if (typeof icon === 'object' && icon && 'light' in icon && 'dark' in icon) { + const castedIcon = (icon as { light: unknown, dark: unknown }); + if ((URI.isUri(castedIcon.light) || this._isUriComponents(castedIcon.light)) && (URI.isUri(castedIcon.dark) || this._isUriComponents(castedIcon.dark))) { + return { light: URI.revive(castedIcon.light), dark: URI.revive(castedIcon.dark) }; + } + } + return undefined; + } + + private _isUriComponents(thing: unknown): thing is UriComponents { + if (!thing) { + return false; + } + return typeof (thing).path === 'string' && + typeof (thing).scheme === 'string'; + } + private async _getUnresolvedDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { // If automation shell is allowed, prefer that if (options.allowAutomationShell) { @@ -315,18 +344,18 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro } } - private _guessProfileIcon(shell: string): string | undefined { + private _guessProfileIcon(shell: string): ThemeIcon | undefined { const file = path.parse(shell).name; switch (file) { case 'bash': - return Codicon.terminalBash.id; + return Codicon.terminalBash; case 'pwsh': case 'powershell': - return Codicon.terminalPowershell.id; + return Codicon.terminalPowershell; case 'tmux': - return Codicon.terminalTmux.id; + return Codicon.terminalTmux; case 'cmd': - return Codicon.terminalCmd.id; + return Codicon.terminalCmd; default: return undefined; } @@ -389,7 +418,7 @@ export class BrowserTerminalProfileResolverService extends BaseTerminalProfileRe @IRemoteTerminalService remoteTerminalService: IRemoteTerminalService, @ITerminalService terminalService: ITerminalService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService ) { super( { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts index d5fd6d9db0a..d62c06f238c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts @@ -10,8 +10,9 @@ import { matchesFuzzy } from 'vs/base/common/filters'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { killTerminalIcon, renameTerminalIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; +import { getColorClass, getIconId, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; export class TerminalQuickAccessProvider extends PickerQuickAccessProvider { @@ -20,6 +21,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider { switch (buttonIndex) { case 0: diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index ab0e1e585c6..269bf44f401 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -418,7 +418,7 @@ export class TerminalService implements ITerminalService { if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.icon) { return; } - this._offProcessTerminalService?.updateIcon(instance.persistentProcessId, instance.icon.id, instance.color); + this._offProcessTerminalService?.updateIcon(instance.persistentProcessId, instance.icon, instance.color); } private _removeGroup(group: ITerminalGroup): void { @@ -964,7 +964,7 @@ export class TerminalService implements ITerminalService { iconClass: ThemeIcon.asClassName(configureTerminalProfileIcon), tooltip: nls.localize('createQuickLaunchProfile', "Configure Terminal Profile") }]; - const icon = profile.icon ? (iconRegistry.get(profile.icon) || Codicon.terminal) : Codicon.terminal; + const icon = (profile.icon && ThemeIcon.isThemeIcon(profile.icon)) ? profile.icon : Codicon.terminal; const label = `$(${icon.id}) ${profile.profileName}`; if (profile.args) { if (typeof profile.args === 'string') { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index f1845c45f5a..70238c70884 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -32,6 +32,7 @@ import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; import { disposableTimeout } from 'vs/base/common/async'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { URI } from 'vs/base/common/uri'; +import { getColorClass, getIconId, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { Schemas } from 'vs/base/common/network'; const $ = DOM.$; @@ -60,7 +61,8 @@ export class TerminalTabList extends WorkbenchList { @ITerminalService private _terminalService: ITerminalService, @ITerminalInstanceService _terminalInstanceService: ITerminalInstanceService, @IInstantiationService instantiationService: IInstantiationService, - @IDecorationsService _decorationsService: IDecorationsService + @IDecorationsService _decorationsService: IDecorationsService, + @IThemeService private readonly _themeService: IThemeService ) { super('TerminalTabsList', container, { @@ -91,6 +93,8 @@ export class TerminalTabList extends WorkbenchList { this._terminalService.onInstanceTitleChanged(() => this.render()); this._terminalService.onInstanceIconChanged(() => this.render()); this._terminalService.onInstancePrimaryStatusChanged(() => this.render()); + this._terminalService.onDidChangeConnectionState(() => this.render()); + this._themeService.onDidColorThemeChange(() => this.render()); this._terminalService.onActiveInstanceChanged(e => { if (e) { const i = this._terminalService.terminalInstances.indexOf(e); @@ -176,7 +180,8 @@ class TerminalTabsRenderer implements IListRenderer Severity.Ignore) { - label = `${prefix}$(${primaryStatus.icon?.id || instance.icon?.id})`; + label = `${prefix}$(${primaryStatus.icon?.id || iconId})`; } else { - label = `${prefix}$(${instance.icon?.id})`; + label = `${prefix}$(${iconId})`; } } else { this.fillActionBar(instance, template); - label = `${prefix}$(${instance.icon?.id})`; + label = `${prefix}$(${iconId})`; // Only add the title if the icon is set, this prevents the title jumping around for // example when launching with a ShellLaunchConfig.name and no icon if (instance.icon) { @@ -275,12 +280,6 @@ class TerminalTabsRenderer implements IListRenderer('.codicon'); - if (codicon) { - codicon.style.color = instance?.icon?.color?.id || ''; - } - - if (!hasActionbar) { template.actionBar.clear(); } @@ -296,6 +295,16 @@ class TerminalTabsRenderer implements IListRenderer container.removeChild(this._styleElement))); + this.updateStyles(); + } + + private _registerListeners(): void { + this._register(this._terminalService.onInstanceIconChanged(() => this.updateStyles())); + this._register(this._terminalService.onInstancesChanged(() => this.updateStyles())); + } + + override updateStyles(): void { + super.updateStyles(); + let css = ''; + // TODO add a rule collector to avoid duplication + for (const instance of this._terminalService.terminalInstances) { + const icon = instance.icon; + if (!icon) { + return; + } + let uri = undefined; + if (icon instanceof URI) { + uri = icon; + } else if (icon instanceof Object && 'light' in icon && 'dark' in icon) { + uri = this._themeService.getColorTheme().type === ColorScheme.LIGHT ? icon.light : icon.dark; + } + const iconClasses = getUriClasses(instance, this._themeService.getColorTheme().type); + if (uri instanceof URI && iconClasses && iconClasses.length > 1) { + css += `.monaco-workbench .${iconClasses[0]} .monaco-highlighted-label .codicon, .monaco-action-bar .terminal-uri-icon.single-terminal-tab.action-label:not(.alt-command) .codicon {`; + css += `background-image: ${dom.asCSSUrl(uri)};}`; + } + } + this._styleElement.textContent = css; + } +} diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index 3c9c2b9e797..633d8c00770 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -18,7 +18,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Schemas } from 'vs/base/common/network'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; @@ -266,7 +266,7 @@ export class RemoteTerminalChannelClient { return this._channel.call('$updateTitle', [id, title]); } - updateIcon(id: number, icon: string, color?: string): Promise { + updateIcon(id: number, icon: TerminalIcon, color?: string): Promise { return this._channel.call('$updateIcon', [id, icon, color]); } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 6fb4a49568c..4788b47c4f1 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -12,6 +12,7 @@ import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/comm import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; export const TERMINAL_VIEW_ID = 'terminal'; @@ -234,7 +235,7 @@ export interface IRemoteTerminalAttachTarget { workspaceId: string; workspaceName: string; isOrphan: boolean; - icon: string | undefined; + icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } } | undefined; color: string | undefined; } diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts index 7bdaa59e580..ed79d042d24 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts @@ -8,6 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -118,7 +119,7 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe await this._localPtyService.updateTitle(id, title); } - async updateIcon(id: number, icon: string, color?: string): Promise { + async updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { await this._localPtyService.updateIcon(id, icon, color); } diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts index 8c61df06052..93363863f9e 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts @@ -24,7 +24,7 @@ export class ElectronTerminalProfileResolverService extends BaseTerminalProfileR @ILocalTerminalService localTerminalService: ILocalTerminalService, @IRemoteTerminalService remoteTerminalService: IRemoteTerminalService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService ) { super( { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 2414d2220b9..dad91802038 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1611,7 +1611,7 @@ export class TestLocalTerminalService implements ILocalTerminalService { async reduceConnectionGraceTime(): Promise { throw new Error('Method not implemented.'); } processBinary(id: number, data: string): Promise { throw new Error('Method not implemented.'); } updateTitle(id: number, title: string): Promise { throw new Error('Method not implemented.'); } - updateIcon(id: number, icon: string, color?: string): Promise { throw new Error('Method not implemented.'); } + updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { throw new Error('Method not implemented.'); } } class TestTerminalChildProcess implements ITerminalChildProcess { From 18ea5ac15e6c109184d70eb5d15eeee1a3ca096f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 20 May 2021 11:30:04 -0700 Subject: [PATCH 211/270] Use isRemote instead of remote authority to determine where to load from --- src/vs/workbench/api/common/extHostCodeInsets.ts | 3 +-- src/vs/workbench/api/common/extHostNotebookKernels.ts | 3 +-- src/vs/workbench/api/common/extHostWebview.ts | 3 +-- src/vs/workbench/api/common/shared/webview.ts | 5 ++++- src/vs/workbench/test/browser/api/extHostWebview.test.ts | 5 +++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 64650ee5ccf..7902345e471 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -12,7 +12,6 @@ import type * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import { generateUuid } from 'vs/base/common/uuid'; -import { Schemas } from 'vs/base/common/network'; export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { @@ -67,7 +66,7 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { private _options: vscode.WebviewOptions = Object.create(null); asWebviewUri(resource: vscode.Uri): vscode.Uri { - const remoteAuthority = extension.extensionLocation.scheme === Schemas.vscodeRemote + const remoteAuthority = that._initData.remote.isRemote ? that._initData.remote.authority : undefined; return asWebviewUri(this._uuid, resource, remoteAuthority); diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 322707160f4..b57dc5f73a0 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -20,7 +20,6 @@ import { CellEditType, IImmediateCellEditOperation, NotebookCellExecutionState, import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { asArray } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; -import { Schemas } from 'vs/base/common/network'; interface IKernelData { extensionId: ExtensionIdentifier, @@ -191,7 +190,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); }, asWebviewUri(uri: URI) { - const remoteAuthority = extension.extensionLocation.scheme === Schemas.vscodeRemote + const remoteAuthority = that._initData.remote.isRemote ? that._initData.remote.authority : undefined; return asWebviewUri(String(handle), uri, remoteAuthority); diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index ce4ce39d8cb..f3845612563 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -5,7 +5,6 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; -import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; @@ -70,7 +69,7 @@ export class ExtHostWebview implements vscode.Webview { public asWebviewUri(resource: vscode.Uri): vscode.Uri { this.#hasCalledAsWebviewUri = true; - const remoteAuthority = this.#extension.extensionLocation.scheme === Schemas.vscodeRemote + const remoteAuthority = this.#initData.remote.isRemote ? this.#initData.remote.authority : undefined; return asWebviewUri(this.#handle, resource, remoteAuthority); diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index 13ec3d02943..2d26fd0ca52 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -8,7 +8,10 @@ import { URI } from 'vs/base/common/uri'; import type * as vscode from 'vscode'; export interface WebviewInitData { - readonly remote: { readonly authority: string | undefined }; + readonly remote: { + readonly isRemote: boolean; + readonly authority: string | undefined + }; } /** diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index bbdee0feefd..4dd59f11662 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -31,7 +31,7 @@ suite('ExtHostWebview', () => { test('Cannot register multiple serializers for the same view type', async () => { const viewType = 'view.type'; - const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { remote: { authority: undefined } }, undefined, new NullLogService(), NullApiDeprecationService); + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { remote: { authority: undefined, isRemote: false } }, undefined, new NullLogService(), NullApiDeprecationService); const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); @@ -123,7 +123,8 @@ suite('ExtHostWebview', () => { function createWebview(rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined, remoteAuthority: string | undefined) { const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { remote: { - authority: remoteAuthority + authority: remoteAuthority, + isRemote: !!remoteAuthority, }, }, undefined, new NullLogService(), NullApiDeprecationService); From af0c01b1e3617a41f6da30bd1525d8d91dabfc85 Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 20 May 2021 11:47:12 -0700 Subject: [PATCH 212/270] Short title for command contribution. --- src/vs/platform/actions/common/actions.ts | 6 +++++- src/vs/workbench/api/common/menusExtensionPoint.ts | 11 ++++++++++- .../contrib/notebook/browser/notebookEditorToolbar.ts | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 1a7d4f12fca..9938dc6cfe5 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -41,6 +41,7 @@ export type Icon = { dark?: URI; light?: URI; } | ThemeIcon; export interface ICommandAction { id: string; title: string | ICommandActionTitle; + shortTitle?: string | ICommandActionTitle; category?: string | ILocalizedString; tooltip?: string; icon?: Icon; @@ -179,6 +180,7 @@ export class MenuId { export interface IMenuActionOptions { arg?: any; shouldForwardArgs?: boolean; + renderShortTitle?: boolean; } export interface IMenu extends IDisposable { @@ -388,7 +390,9 @@ export class MenuItemAction implements IAction { @ICommandService private _commandService: ICommandService ) { this.id = item.id; - this.label = typeof item.title === 'string' ? item.title : item.title.value; + this.label = options?.renderShortTitle && item.shortTitle + ? (typeof item.shortTitle === 'string' ? item.shortTitle : item.shortTitle.value) + : (typeof item.title === 'string' ? item.title : item.title.value); this.tooltip = item.tooltip ?? ''; this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition); this.checked = false; diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 7b3a989c920..1b4dbf22065 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -438,6 +438,7 @@ namespace schema { export interface IUserFriendlyCommand { command: string; title: string | ILocalizedString; + shortTitle?: string | ILocalizedString; enablement?: string; category?: string | ILocalizedString; icon?: IUserFriendlyIcon; @@ -457,6 +458,9 @@ namespace schema { if (!isValidLocalizedString(command.title, collector, 'title')) { return false; } + if (command.shortTitle && !isValidLocalizedString(command.shortTitle, collector, 'shortTitle')) { + return false; + } if (command.enablement && typeof command.enablement !== 'string') { collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'precondition')); return false; @@ -510,6 +514,10 @@ namespace schema { description: localize('vscode.extension.contributes.commandType.title', 'Title by which the command is represented in the UI'), type: 'string' }, + shortTitle: { + description: localize('vscode.extension.contributes.commandType.shortTitle', 'Short title by which the command is represented in the UI'), + type: 'string' + }, category: { description: localize('vscode.extension.contributes.commandType.category', '(Optional) Category string by the command is grouped in the UI'), type: 'string' @@ -567,7 +575,7 @@ commandsExtensionPoint.setHandler(extensions => { return; } - const { icon, enablement, category, title, command } = userFriendlyCommand; + const { icon, enablement, category, title, shortTitle, command } = userFriendlyCommand; let absoluteIcon: { dark: URI; light?: URI; } | ThemeIcon | undefined; if (icon) { @@ -588,6 +596,7 @@ commandsExtensionPoint.setHandler(extensions => { bucket.push({ id: command, title, + shortTitle, category, precondition: ContextKeyExpr.deserialize(enablement), icon: absoluteIcon diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts index 0d45a0ab1dd..0e2fd14c3cd 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts @@ -164,7 +164,7 @@ export class NotebookEditorToolbar extends Disposable { this.domNode.style.display = 'none'; } else { this._notebookLeftToolbar.setActions([], []); - const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true }); + const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true, renderShortTitle: true }); this.domNode.style.display = 'flex'; const primaryLeftGroups = groups.filter(group => /^navigation/.test(group[0])); let primaryActions: IAction[] = []; From 514d3162bc82bc9bc50c4aba0992f1386ad3d3d4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 20 May 2021 11:47:23 -0700 Subject: [PATCH 213/270] Persist title source and set after reconnect Fixes #123783 --- src/vs/platform/terminal/common/terminal.ts | 17 +++++++++++++---- .../terminal/common/terminalProcess.ts | 3 ++- .../platform/terminal/node/ptyHostService.ts | 6 +++--- src/vs/platform/terminal/node/ptyService.ts | 19 ++++++++++--------- .../api/browser/mainThreadTerminalService.ts | 4 ++-- .../terminal/browser/remoteTerminalService.ts | 11 +++++++---- .../contrib/terminal/browser/terminal.ts | 9 +++++++-- .../terminal/browser/terminalActions.ts | 4 ++-- .../terminal/browser/terminalInstance.ts | 12 ++++++++++-- .../terminal/browser/terminalService.ts | 10 +++------- .../contrib/terminal/common/terminal.ts | 12 ++---------- .../electron-sandbox/localTerminalService.ts | 6 +++--- 12 files changed, 64 insertions(+), 49 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 3c9c10db0d3..82b064a99af 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -103,7 +103,6 @@ export interface IRawTerminalTabLayoutInfo { } export type ITerminalTabLayoutInfoById = IRawTerminalTabLayoutInfo; -export type ITerminalTabLayoutInfo = IRawTerminalTabLayoutInfo; export interface IRawTerminalsLayoutInfo { tabs: IRawTerminalTabLayoutInfo[]; @@ -113,6 +112,7 @@ export interface IPtyHostAttachTarget { id: number; pid: number; title: string; + titleSource: TitleEventSource; cwd: string; workspaceId: string; workspaceName: string; @@ -120,6 +120,15 @@ export interface IPtyHostAttachTarget { icon: TerminalIcon | undefined; } +export enum TitleEventSource { + /** From the API or the rename command that overrides any other type */ + Api, + /** From the process name property*/ + Process, + /** From the VT sequence */ + Sequence +} + export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo; export type ITerminalsLayoutInfoById = IRawTerminalsLayoutInfo; @@ -174,7 +183,7 @@ export interface IOffProcessTerminalService { getEnvironment(): Promise; getShellEnvironment(): Promise; setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise; - updateTitle(id: number, title: string): Promise; + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise; updateIcon(id: number, icon: TerminalIcon, color?: string): Promise; getTerminalLayoutInfo(): Promise; reduceConnectionGraceTime(): Promise; @@ -240,7 +249,7 @@ export interface IPtyService { processBinary(id: number, data: string): Promise; /** Confirm the process is _not_ an orphan. */ orphanQuestionReply(id: number): Promise; - updateTitle(id: number, title: string): Promise; + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise; updateIcon(id: number, icon: TerminalIcon, color?: string): Promise; getDefaultSystemShell(osOverride?: OperatingSystem): Promise; getProfiles?(includeDetectedProfiles?: boolean): Promise; @@ -349,7 +358,7 @@ export interface IShellLaunchConfig { /** * This is a terminal that attaches to an already running terminal. */ - attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; icon?: TerminalIcon; color?: string }; + attachPersistentProcess?: { id: number; pid: number; title: string; titleSource: TitleEventSource; cwd: string; icon?: TerminalIcon; color?: string }; /** * Whether the terminal process environment should be exactly as provided in diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts index 914e85c8a94..bc51bd66e7d 100644 --- a/src/vs/platform/terminal/common/terminalProcess.ts +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { UriComponents } from 'vs/base/common/uri'; -import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon } from 'vs/platform/terminal/common/terminal'; +import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; export interface ISingleTerminalConfiguration { @@ -51,6 +51,7 @@ export interface IProcessDetails { id: number; pid: number; title: string; + titleSource: TitleEventSource; cwd: string; workspaceId: string; workspaceName: string; diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index b9ad90e8602..27eb9176773 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -5,7 +5,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType, ITerminalProfile, IRequestResolveVariablesEvent, SafeConfigProvider, TerminalSettingId, TerminalIcon } from 'vs/platform/terminal/common/terminal'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType, ITerminalProfile, IRequestResolveVariablesEvent, SafeConfigProvider, TerminalSettingId, TitleEventSource, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { FileAccess } from 'vs/base/common/network'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; @@ -167,8 +167,8 @@ export class PtyHostService extends Disposable implements IPtyService { lastPtyId = Math.max(lastPtyId, id); return id; } - updateTitle(id: number, title: string): Promise { - return this._proxy.updateTitle(id, title); + updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { + return this._proxy.updateTitle(id, title, titleSource); } updateIcon(id: number, icon: TerminalIcon, color?: string): Promise { return this._proxy.updateIcon(id, icon, color); diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 950d09a069a..f10f2af6d6f 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -5,7 +5,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType, IProcessReadyEvent, TerminalIcon } from 'vs/platform/terminal/common/terminal'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType, IProcessReadyEvent, TitleEventSource, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; @@ -115,8 +115,8 @@ export class PtyService extends Disposable implements IPtyService { } } - async updateTitle(id: number, title: string): Promise { - this._throwIfNoPty(id).setTitle(title); + async updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { + this._throwIfNoPty(id).setTitle(title, titleSource); } async updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { @@ -205,10 +205,8 @@ export class PtyService extends Disposable implements IPtyService { const layout = this._workspaceLayoutInfos.get(args.workspaceId); if (layout) { const expandedTabs = await Promise.all(layout.tabs.map(async tab => this._expandTerminalTab(tab))); - const filtered = expandedTabs.filter(t => t.terminals.length > 0); - return { - tabs: filtered - }; + const tabs = expandedTabs.filter(t => t.terminals.length > 0); + return { tabs }; } return undefined; } @@ -246,6 +244,7 @@ export class PtyService extends Disposable implements IPtyService { return { id, title: persistentProcess.title, + titleSource: persistentProcess.titleSource, pid: persistentProcess.pid, workspaceId: persistentProcess.workspaceId, workspaceName: persistentProcess.workspaceName, @@ -300,15 +299,17 @@ export class PersistentTerminalProcess extends Disposable { private _pid = -1; private _cwd = ''; private _title: string | undefined; - + private _titleSource: TitleEventSource = TitleEventSource.Process; get pid(): number { return this._pid; } get title(): string { return this._title || this._terminalProcess.currentTitle; } + get titleSource(): TitleEventSource { return this._titleSource; } get icon(): TerminalIcon | undefined { return this._icon; } get color(): string | undefined { return this._color; } - setTitle(title: string): void { + setTitle(title: string, titleSource: TitleEventSource): void { this._title = title; + this._titleSource = titleSource; } setIcon(icon: TerminalIcon, color?: string): void { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 7c81d6928d0..56d4075670c 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -10,13 +10,13 @@ import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalDimensions, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { ITerminalExternalLinkProvider, ITerminalInstance, ITerminalInstanceService, ITerminalLink, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; -import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileResolverService, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { OperatingSystem, OS } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index 3ed3747df3d..619b98e9212 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -17,11 +17,12 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IRequestResolveVariablesEvent, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon } from 'vs/platform/terminal/common/terminal'; +import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { RemotePty } from 'vs/workbench/contrib/terminal/browser/remotePty'; import { IRemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ICompleteTerminalConfiguration, RemoteTerminalChannelClient, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; -import { IRemoteTerminalAttachTarget, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -195,18 +196,20 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal return undefined; } - async listProcesses(): Promise { + async listProcesses(): Promise { const terms = this._remoteTerminalChannel ? await this._remoteTerminalChannel.listProcesses() : []; return terms.map(termDto => { - return { + return { id: termDto.id, pid: termDto.pid, title: termDto.title, + titleSource: termDto.titleSource, cwd: termDto.cwd, workspaceId: termDto.workspaceId, workspaceName: termDto.workspaceName, icon: termDto.icon, - color: termDto.color + color: termDto.color, + isOrphan: termDto.isOrphan }; }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 6c2315fc798..0268f12c9bc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -8,8 +8,8 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IOffProcessTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TerminalShellType } from 'vs/platform/terminal/common/terminal'; -import { ICommandTracker, INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IOffProcessTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { ICommandTracker, INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; @@ -378,6 +378,11 @@ export interface ITerminalInstance { */ readonly title: string; + /** + * How the current title was set. + */ + readonly titleSource: TitleEventSource; + /** * The shell type of the terminal. */ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index c0a7b7ae945..a3def71891b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -26,13 +26,13 @@ import { IListService } from 'vs/platform/list/browser/listService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPickOptions, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { ILocalTerminalService, ITerminalProfile, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { ILocalTerminalService, ITerminalProfile, TerminalSettingId, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; import { Direction, IRemoteTerminalService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; -import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TABS_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_ACTION_CATEGORY, TerminalCommandId, TERMINAL_VIEW_ID, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TABS_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_ACTION_CATEGORY, TerminalCommandId, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index fb2acc2531f..f715d7b2029 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -24,7 +24,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, DEFAULT_COMMANDS_TO_SKIP_SHELL, TERMINAL_CREATION_COMMANDS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, SUGGESTED_RENDERER_TYPE, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, DEFAULT_COMMANDS_TO_SKIP_SHELL, TERMINAL_CREATION_COMMANDS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, SUGGESTED_RENDERER_TYPE, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { ansiColorIdentifiers, ansiColorMap, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; @@ -46,7 +46,7 @@ import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTy import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TerminalIcon } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType, TerminalSettingId, TitleEventSource, TerminalIcon } from 'vs/platform/terminal/common/terminal'; import { IProductService } from 'vs/platform/product/common/productService'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { AutoOpenBarrier } from 'vs/base/common/async'; @@ -105,6 +105,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _skipTerminalCommands: string[]; private _shellType: TerminalShellType; private _title: string = ''; + private _titleSource: TitleEventSource = TitleEventSource.Process; private _container: HTMLElement | undefined; private _wrapperElement: (HTMLElement & { xterm?: XTermTerminal }) | undefined; private _xterm: XTermTerminal | undefined; @@ -187,6 +188,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get isDisconnected(): boolean { return this._processManager.isDisconnected; } get isRemote(): boolean { return this._processManager.remoteAuthority !== undefined; } get title(): string { return this._title; } + get titleSource(): TitleEventSource { return this._titleSource; } get icon(): TerminalIcon | undefined { return this._getIcon(); } get color(): string | undefined { return this._getColor(); } @@ -286,6 +288,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Wait for a period to allow a container to be ready await this._containerReadyBarrier.wait(); await this._createProcess(); + + // Re-establish the title after reconnect + if (this.shellLaunchConfig.attachPersistentProcess) { + this.setTitle(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); + } }); this.addDisposable(this._configurationService.onDidChangeConfiguration(e => { @@ -1623,6 +1630,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } const didTitleChange = title !== this._title; this._title = title; + this._titleSource = eventSource; if (didTitleChange) { this._setAriaLabel(this._xterm, this._instanceId, this._title); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 269bf44f401..e2e19ad7fbb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -294,12 +294,8 @@ export class TerminalService implements ITerminalService { // The state must be updated when the terminal is relaunched, otherwise the persistent // terminal ID will be stale and the process will be leaked. this.onInstanceProcessIdReady(() => this._saveState()); - this.onInstanceTitleChanged(instance => { - this._updateTitle(instance); - }); - this.onInstanceIconChanged(instance => { - this._updateIcon(instance); - }); + this.onInstanceTitleChanged(instance => this._updateTitle(instance)); + this.onInstanceIconChanged(instance => this._updateIcon(instance)); } private _handleInstanceContextKeys(): void { @@ -410,7 +406,7 @@ export class TerminalService implements ITerminalService { if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.title) { return; } - this._offProcessTerminalService?.updateTitle(instance.persistentProcessId, instance.title); + this._offProcessTerminalService?.updateTitle(instance.persistentProcessId, instance.title, instance.titleSource); } @debounce(500) diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 4788b47c4f1..14571020725 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -9,7 +9,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -231,6 +231,7 @@ export interface IRemoteTerminalAttachTarget { id: number; pid: number; title: string; + titleSource: TitleEventSource; cwd: string; workspaceId: string; workspaceName: string; @@ -364,15 +365,6 @@ export interface IDefaultShellAndArgsRequest { callback: (shell: string, args: string[] | string | undefined) => void; } -export enum TitleEventSource { - /** From the API or the rename command that overrides any other type */ - Api, - /** From the process name property*/ - Process, - /** From the VT sequence */ - Sequence -} - export const QUICK_LAUNCH_PROFILE_CHOICE = 'workbench.action.terminal.profile.choice'; export const enum TerminalCommandId { diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts index ed79d042d24..18feaabdacd 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts @@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { ILocalTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { ILocalTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -115,8 +115,8 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe })); } } - async updateTitle(id: number, title: string): Promise { - await this._localPtyService.updateTitle(id, title); + async updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { + await this._localPtyService.updateTitle(id, title, titleSource); } async updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { From 4c1474b458de0316a015b875fe9b0b4d95b9089e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 20 May 2021 12:47:17 -0700 Subject: [PATCH 214/270] debug: finalize parentSession Closes https://github.com/microsoft/vscode/issues/123403 --- src/vs/vscode.d.ts | 6 ++++++ src/vs/vscode.proposed.d.ts | 12 ------------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 118837f3aad..a2321c80c67 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -11607,6 +11607,12 @@ declare module 'vscode' { */ readonly type: string; + /** + * The parent session of this debug session, if it was created as a child. + * @see DebugSessionOptions.parentSession + */ + readonly parentSession?: DebugSession; + /** * The debug session's name is initially taken from the {@link DebugConfiguration debug configuration}. * Any changes will be properly reflected in the UI. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 138eb225334..b96c5596b97 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -3281,18 +3281,6 @@ declare module 'vscode' { //#endregion - //#region Expose parent session on DebugSessions - https://github.com/microsoft/vscode/issues/123403#issuecomment-843269200 - - export interface DebugSession { - /** - * The parent session of this debug session, if it was created as a child. - * @see DebugSessionOptions.parentSession - */ - readonly parentSession?: DebugSession; - } - - //#endregion - //#region https://github.com/microsoft/vscode/issues/87110 @eamodio export interface Memento { From ddf8cc42616f82e4a7d6f6a174158d4a897075fd Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 20 May 2021 12:48:28 -0700 Subject: [PATCH 215/270] Dispose CKS --- .../contrib/statusBar/executionStatusBarItemController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts index 88092be185a..e32ba0518d5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/executionStatusBarItemController.ts @@ -243,7 +243,7 @@ class KeybindingPlaceholderStatusBarHelper extends Disposable { super(); // Create a fake ContextKeyService, and look up the keybindings within this context. - this._contextKeyService = _contextKeyService.createScoped(document.createElement('div')); + this._contextKeyService = this._register(_contextKeyService.createScoped(document.createElement('div'))); InputFocusedContext.bindTo(this._contextKeyService).set(true); EditorContextKeys.editorTextFocus.bindTo(this._contextKeyService).set(true); EditorContextKeys.focus.bindTo(this._contextKeyService).set(true); From 97740a7d253650f9f186c211de5247e2577ce9f7 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Thu, 20 May 2021 15:51:30 -0400 Subject: [PATCH 216/270] Revert "Revert "Revert "Closes #122433""" This reverts commit 6b097212e69119c2b147ee8bf3939c9c04490808. --- .../editor/browser/editorOverrideService.ts | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 45c897382db..990a4acf6d9 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -40,13 +40,9 @@ type ContributionPoints = Array; export class EditorOverrideService extends Disposable implements IEditorOverrideService { readonly _serviceBrand: undefined; - // Constants private static readonly configureDefaultID = 'promptOpenWith.configureDefault'; - private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; - private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; - - // Data Stores private _contributionPoints: Map = new Map(); + private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; private cache: Set | undefined; constructor( @@ -133,8 +129,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } const input = await this.doOverrideEditorInput(editor, options, group, selectedContribution); if (conflictingDefault && input) { - // Show the conflicting default dialog - await this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); + // Wait one second to give the user ample time to see the current editor then ask them to configure a default + this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); } // Add the group as we might've changed it with the quickpick @@ -371,23 +367,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } private async doHandleConflictingDefaults(editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { - type StoredChoice = { - [key: string]: string[]; + const makeCurrentEditorDefault = () => { + const viewType = currentEditor.viewType; + if (viewType) { + this.updateUserAssociations(`*${extname(currentEditor.resource!)}`, viewType); + } }; - const contributionPoints = this.findMatchingContributions(currentEditor.resource!); - const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorOverrideService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); - const globForResource = `*${extname(currentEditor.resource!)}`; - // Writes to the storage service that a choice has been made for the currently installed editors - const writeCurrentEditorsToStorage = () => { - storedChoices[globForResource] = []; - contributionPoints.forEach(contrib => storedChoices[globForResource].push(contrib.editorInfo.id)); - this.storageService.store(EditorOverrideService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); - }; - - // If the user has already made a choice for this editor we don't want to ask them again - if (storedChoices[globForResource]?.find(editorID => editorID === currentEditor.viewType)) { - return; - } const handle = this.notificationService.prompt(Severity.Warning, localize('editorOverride.conflictingDefaults', 'There are multiple default editors available for the resource.'), @@ -415,12 +400,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }, { label: localize('editorOverride.keepDefault', 'Keep {0}', editorName), - run: writeCurrentEditorsToStorage + run: makeCurrentEditorDefault } ]); // If the user pressed X we assume they want to keep the current editor as default const onCloseListener = handle.onDidClose(() => { - writeCurrentEditorsToStorage(); + makeCurrentEditorDefault(); onCloseListener.dispose(); }); } From 9627b4ea63e5ea634825882cda7577750be424c5 Mon Sep 17 00:00:00 2001 From: Raymond Zhao Date: Thu, 20 May 2021 13:20:39 -0700 Subject: [PATCH 217/270] Emmet identify CDATA for wrap, fixes #123136 --- extensions/emmet/src/abbreviationActions.ts | 7 +- .../src/test/wrapWithAbbreviation.test.ts | 73 +++++++++++++++++++ extensions/emmet/src/util.ts | 30 +++++++- 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/extensions/emmet/src/abbreviationActions.ts b/extensions/emmet/src/abbreviationActions.ts index c7c9d84fd30..f5bccfc5b9d 100644 --- a/extensions/emmet/src/abbreviationActions.ts +++ b/extensions/emmet/src/abbreviationActions.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetFlatNode'; -import { getEmmetHelper, getFlatNode, getMappingForIncludedLanguages, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument, isOffsetInsideOpenOrCloseTag } from './util'; +import { getEmmetHelper, getFlatNode, getHtmlFlatNode, getMappingForIncludedLanguages, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument, isOffsetInsideOpenOrCloseTag } from './util'; import { getRootNode as parseDocument } from './parseDocument'; const localize = nls.loadMessageBundle(); @@ -56,7 +56,8 @@ export async function wrapWithAbbreviation(args: any): Promise { let { start, end } = rangeToReplace; const startOffset = document.offsetAt(start); - const startNode = getFlatNode(rootNode, startOffset, true); + const documentText = document.getText(); + const startNode = getHtmlFlatNode(documentText, rootNode, startOffset, true); if (startNode && isOffsetInsideOpenOrCloseTag(startNode, startOffset)) { start = document.positionAt(startNode.start); const nodeEndPosition = document.positionAt(startNode.end); @@ -64,7 +65,7 @@ export async function wrapWithAbbreviation(args: any): Promise { } const endOffset = document.offsetAt(end); - const endNode = getFlatNode(rootNode, endOffset, true); + const endNode = getHtmlFlatNode(documentText, rootNode, endOffset, true); if (endNode && isOffsetInsideOpenOrCloseTag(endNode, endOffset)) { const nodeStartPosition = document.positionAt(endNode.start); start = nodeStartPosition.isBefore(start) ? nodeStartPosition : start; diff --git a/extensions/emmet/src/test/wrapWithAbbreviation.test.ts b/extensions/emmet/src/test/wrapWithAbbreviation.test.ts index 825776c782a..978d154ae97 100644 --- a/extensions/emmet/src/test/wrapWithAbbreviation.test.ts +++ b/extensions/emmet/src/test/wrapWithAbbreviation.test.ts @@ -219,6 +219,79 @@ suite('Tests for Wrap with Abbreviations', () => { return testWrapWithAbbreviation([new Selection(3, 2, 3, 2)], 'div', expectedContents, contents); }); + test('Wrap with abbreviation inner node in cdata', () => { + const contents = ` + +

Test 2

+ ]]> + hello + + `; + const expectedContents = ` + +
+

Test 2

+
+ ]]> + hello + + `; + return testWrapWithAbbreviation([new Selection(6, 5, 6, 5)], 'div', expectedContents, contents); + }); + + test('Wrap with abbreviation inner node in script in cdata', () => { + const contents = ` + + `; + const expectedContents = ` + + `; + return testWrapWithAbbreviation([new Selection(4, 10, 4, 10)], 'div', expectedContents, contents); + }); + + test('Wrap with abbreviation inner node in cdata one-liner', () => { + const contents = ` + + `; + // this result occurs because no selection on the open/close p tag was given + const expectedContents = ` + + `; + return testWrapWithAbbreviation([new Selection(2, 15, 2, 15)], 'div', expectedContents, contents); + }); + test('Wrap with multiline abbreviation doesnt add extra spaces', () => { // Issue #29898 const contents = ` diff --git a/extensions/emmet/src/util.ts b/extensions/emmet/src/util.ts index cb63f504d5d..cb4a876bdcb 100644 --- a/extensions/emmet/src/util.ts +++ b/extensions/emmet/src/util.ts @@ -381,13 +381,19 @@ export function getHtmlFlatNode(documentText: string, root: FlatNode | undefined // If the currentNode is a script one, first set up its subtree and then find HTML node. if (currentNode.name === 'script' && currentNode.children.length === 0) { - setUpScriptNodeSubtree(documentText, currentNode); - currentNode = getFlatNode(currentNode, offset, includeNodeBoundary) ?? currentNode; + const scriptNodeBody = setupScriptNodeSubtree(documentText, currentNode); + if (scriptNodeBody) { + currentNode = getHtmlFlatNode(scriptNodeBody, currentNode, offset, includeNodeBoundary) ?? currentNode; + } + } + else if (currentNode.type === 'cdata') { + const cdataBody = setupCdataNodeSubtree(documentText, currentNode); + currentNode = getHtmlFlatNode(cdataBody, currentNode, offset, includeNodeBoundary) ?? currentNode; } return currentNode; } -export function setUpScriptNodeSubtree(documentText: string, scriptNode: HtmlFlatNode): void { +export function setupScriptNodeSubtree(documentText: string, scriptNode: HtmlFlatNode): string { const isTemplateScript = scriptNode.name === 'script' && (scriptNode.attributes && scriptNode.attributes.some(x => x.name.toString() === 'type' @@ -403,7 +409,25 @@ export function setUpScriptNodeSubtree(documentText: string, scriptNode: HtmlFla scriptNode.children.push(child); child.parent = scriptNode; }); + return scriptBodyText; } + return ''; +} + +export function setupCdataNodeSubtree(documentText: string, cdataNode: HtmlFlatNode): string { + // blank out the rest of the document and generate the subtree. + const cdataStart = ''; + const startToUse = cdataNode.start + cdataStart.length; + const endToUse = cdataNode.end - cdataEnd.length; + const beforePadding = ' '.repeat(startToUse); + const cdataBody = beforePadding + documentText.substring(startToUse, endToUse); + const innerRoot: HtmlFlatNode = parse(cdataBody); + innerRoot.children.forEach(child => { + cdataNode.children.push(child); + child.parent = cdataNode; + }); + return cdataBody; } export function isOffsetInsideOpenOrCloseTag(node: FlatNode, offset: number): boolean { From d3f040fe017e55c19d7ec7246726c70de649f168 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 20 May 2021 14:35:43 -0700 Subject: [PATCH 218/270] Fix notebook cell statusbar items duplicating. Cancel tokens on dispose, and dispose the timeout properly --- .../contributedStatusBarItemController.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts index 5f9e5bbd99d..a625c2e7330 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/contributedStatusBarItemController.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { flatten } from 'vs/base/common/arrays'; -import { Throttler } from 'vs/base/common/async'; +import { disposableTimeout, Throttler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICellVisibilityChangeEvent, NotebookVisibleCellObserver } from 'vs/workbench/contrib/notebook/browser/contrib/statusBar/notebookVisibleCellObserver'; import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; @@ -69,7 +69,7 @@ class CellStatusBarHelper extends Disposable { private _currentItemIds: string[] = []; private _currentItemLists: INotebookCellStatusBarItemList[] = []; - private _activeToken = new MutableDisposable(); + private _activeToken: CancellationTokenSource | undefined; private readonly _updateThrottler = new Throttler(); @@ -80,6 +80,7 @@ class CellStatusBarHelper extends Disposable { ) { super(); + this._register(toDisposable(() => this._activeToken?.dispose(true))); this._updateSoon(); this._register(this._cell.model.onDidChangeContent(() => this._updateSoon())); this._register(this._cell.model.onDidChangeLanguage(() => this._updateSoon())); @@ -90,25 +91,24 @@ class CellStatusBarHelper extends Disposable { private _updateSoon(): void { // Wait a tick to make sure that the event is fired to the EH before triggering status bar providers - setTimeout(() => { + this._register(disposableTimeout(() => { this._updateThrottler.queue(() => this._update()); - }, 0); + }, 0)); } private async _update() { const cellIndex = this._notebookViewModel.getCellIndex(this._cell); const docUri = this._notebookViewModel.notebookDocument.uri; const viewType = this._notebookViewModel.notebookDocument.viewType; - this._activeToken.value = new CancellationTokenSource(); - const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, this._activeToken.value.token); - if (this._activeToken.value.token.isCancellationRequested) { + + this._activeToken?.dispose(true); + const tokenSource = this._activeToken = new CancellationTokenSource(); + const itemLists = await this._notebookCellStatusBarService.getStatusBarItemsForCell(docUri, cellIndex, viewType, tokenSource.token); + if (tokenSource.token.isCancellationRequested) { itemLists.forEach(itemList => itemList.dispose && itemList.dispose()); - this._activeToken.clear(); return; } - this._activeToken.clear(); - const items = flatten(itemLists.map(itemList => itemList.items)); const newIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); From 55c2fc5604eac5396deab13e239d2d0dd954ff01 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Thu, 20 May 2021 15:53:23 -0700 Subject: [PATCH 219/270] add setting and checkbox for untrusted files #wt --- .../browser/workspace.contribution.ts | 15 ++++++- .../workspaces/common/workspaceTrust.ts | 43 ++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 5ceedd2ab87..a05c09c0af7 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -25,7 +25,7 @@ import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarA import { IEditorRegistry, EditorDescriptor } from 'vs/workbench/browser/editor'; import { shieldIcon, WorkspaceTrustEditor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustEditor'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; -import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WORKSPACE_TRUST_UNTRUSTED_FILES } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { EditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, EditorExtensions, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -556,6 +556,19 @@ Registry.as(ConfigurationExtensions.Configuration) localize('workspace.trust.startupPrompt.never', "Do not ask for trust when an untrusted workspace is opened."), ] }, + [WORKSPACE_TRUST_UNTRUSTED_FILES]: { + type: 'string', + default: 'prompt', + included: !isWeb, + description: localize('workspace.trust.untrustedFiles.description', "Controls how to handle opening untrusted files in a trusted workspace."), + scope: ConfigurationScope.APPLICATION, + enum: ['prompt', 'open', 'newWindow'], + enumDescriptions: [ + localize('workspace.trust.untrustedFiles.prompt', "Ask how to handle untrusted files for each workspace. Once untrusted files are introduced to a trusted workspace, you will not be prompted again."), + localize('workspace.trust.untrustedFiles.open', "Always allow untrusted files to be introduced to a trusted workspace without prompting."), + localize('workspace.trust.untrustedFiles.newWindow', "Always open untrusted files in a separate window in restricted mode without prompting."), + ] + }, [WORKSPACE_TRUST_EMPTY_WINDOW]: { type: 'boolean', default: false, diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 82b617dc584..20dd398829b 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -28,6 +28,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt'; +export const WORKSPACE_TRUST_UNTRUSTED_FILES = 'security.workspace.trust.untrustedFiles'; export const WORKSPACE_TRUST_EMPTY_WINDOW = 'security.workspace.trust.emptyWindow'; export const WORKSPACE_TRUST_EXTENSION_SUPPORT = 'extensions.supportUntrustedWorkspaces'; export const WORKSPACE_TRUST_STORAGE_KEY = 'content.trust.model.key'; @@ -353,6 +354,7 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa constructor( @IContextKeyService contextKeyService: IContextKeyService, @IDialogService private readonly dialogService: IDialogService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { @@ -374,6 +376,14 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa this._ctxWorkspaceTrustState.set(trusted); } + private get untrustedFilesSetting(): 'prompt' | 'open' | 'newWindow' { + return this.configurationService.getValue(WORKSPACE_TRUST_UNTRUSTED_FILES); + } + + private set untrustedFilesSetting(value: 'prompt' | 'open' | 'newWindow') { + this.configurationService.updateValue(WORKSPACE_TRUST_UNTRUSTED_FILES, value); + } + private resolveRequest(trusted?: boolean): void { if (this._modalTrustRequestResolver) { this._modalTrustRequestResolver(trusted ?? this.trusted); @@ -418,6 +428,17 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa return WorkspaceTrustUriResponse.Open; } + // If user has setting, don't need to ask + if (this.untrustedFilesSetting !== 'prompt') { + if (this.untrustedFilesSetting === 'newWindow') { + return WorkspaceTrustUriResponse.OpenInNewWindow; + } + + if (this.untrustedFilesSetting === 'open') { + return WorkspaceTrustUriResponse.Open; + } + } + // If we already asked the user, don't need to ask again if (this.workspaceTrustManagementService.acceptsOutOfWorkspaceFiles) { return WorkspaceTrustUriResponse.Open; @@ -432,18 +453,36 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Do you trust the authors of these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Restricted Mode Window"), localize('cancel', "Cancel")], { cancelId: 2, + checkbox: { + label: localize('openLooseFileWorkspaceCheckbox', "Remember my decision for all workspaces"), + checked: false + }, custom: { icon: Codicon.shield, markdownDetails: markdownDetails.map(md => { return { markdown: new MarkdownString(md) }; }) } }); + const saveResponseIfChecked = (response: WorkspaceTrustUriResponse, checked: boolean) => { + if (checked) { + if (response === WorkspaceTrustUriResponse.Open) { + this.untrustedFilesSetting = 'open'; + } + + if (response === WorkspaceTrustUriResponse.OpenInNewWindow) { + this.untrustedFilesSetting = 'newWindow'; + } + } + + return response; + }; + switch (result.choice) { case 0: this.workspaceTrustManagementService.acceptsOutOfWorkspaceFiles = true; - return WorkspaceTrustUriResponse.Open; + return saveResponseIfChecked(WorkspaceTrustUriResponse.Open, !!result.checkboxChecked); case 1: - return WorkspaceTrustUriResponse.OpenInNewWindow; + return saveResponseIfChecked(WorkspaceTrustUriResponse.OpenInNewWindow, !!result.checkboxChecked); default: return WorkspaceTrustUriResponse.Cancel; } From 07a2aab8f2c740e3970df0a523edbee21c0f79f4 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 20 May 2021 16:00:31 -0700 Subject: [PATCH 220/270] Remove duplication around checking isRemote --- src/vs/workbench/api/common/extHostCodeInsets.ts | 5 +---- src/vs/workbench/api/common/extHostNotebookKernels.ts | 5 +---- src/vs/workbench/api/common/extHostWebview.ts | 5 +---- src/vs/workbench/api/common/shared/webview.ts | 8 ++++---- .../notebook/browser/view/renderers/backLayerWebView.ts | 3 +-- .../contrib/webview/browser/baseWebviewElement.ts | 5 +++-- .../welcome/gettingStarted/browser/gettingStarted.ts | 2 +- 7 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 7902345e471..291d6fbd4fc 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -66,10 +66,7 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { private _options: vscode.WebviewOptions = Object.create(null); asWebviewUri(resource: vscode.Uri): vscode.Uri { - const remoteAuthority = that._initData.remote.isRemote - ? that._initData.remote.authority - : undefined; - return asWebviewUri(this._uuid, resource, remoteAuthority); + return asWebviewUri(this._uuid, resource, that._initData.remote); } get cspSource(): string { diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index b57dc5f73a0..e342c86c04e 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -190,10 +190,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); }, asWebviewUri(uri: URI) { - const remoteAuthority = that._initData.remote.isRemote - ? that._initData.remote.authority - : undefined; - return asWebviewUri(String(handle), uri, remoteAuthority); + return asWebviewUri(String(handle), uri, that._initData.remote); }, // --- priority updateNotebookAffinity(notebook, priority) { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index f3845612563..0aabbf99f3f 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -69,10 +69,7 @@ export class ExtHostWebview implements vscode.Webview { public asWebviewUri(resource: vscode.Uri): vscode.Uri { this.#hasCalledAsWebviewUri = true; - const remoteAuthority = this.#initData.remote.isRemote - ? this.#initData.remote.authority - : undefined; - return asWebviewUri(this.#handle, resource, remoteAuthority); + return asWebviewUri(this.#handle, resource, this.#initData.remote); } public get cspSource(): string { diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index 2d26fd0ca52..eeecb0ca1f1 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -38,21 +38,21 @@ export const webviewGenericCspSource = 'https://*.vscode-webview-test.com'; * * @param uuid Unique id of the webview. * @param resource Uri of the resource to load. - * @param fromAuthority Optional remote authority that specifies where `resource` should be resolved from. + * @param remoteInfo Optional information about the remote that specifies where `resource` should be resolved from. */ export function asWebviewUri( uuid: string, resource: vscode.Uri, - fromAuthority: string | undefined + remoteInfo?: { authority: string | undefined, isRemote: boolean } ): vscode.Uri { if (resource.scheme === Schemas.http || resource.scheme === Schemas.https) { return resource; } - if (fromAuthority && resource.scheme === Schemas.file) { + if (remoteInfo && remoteInfo.authority && remoteInfo.isRemote && resource.scheme === Schemas.file) { resource = URI.from({ scheme: Schemas.vscodeRemote, - authority: fromAuthority, + authority: remoteInfo.authority, path: resource.path, }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 58dc2410754..323b6161a34 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -787,8 +787,7 @@ export class BackLayerWebView extends Disposable { } private asWebviewUri(uri: URI, fromExtension: URI | undefined) { - const remoteAuthority = fromExtension?.scheme === Schemas.vscodeRemote ? fromExtension.authority : undefined; - return asWebviewUri(this.id, uri, remoteAuthority); + return asWebviewUri(this.id, uri, fromExtension?.scheme === Schemas.vscodeRemote ? { isRemote: true, authority: fromExtension.authority } : undefined); } postKernelMessage(message: any) { diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 1653f9bff32..ed405e35c00 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -379,6 +379,7 @@ export abstract class BaseWebview extends Disposable { } private rewriteVsCodeResourceUrls(value: string): string { + const isRemote = this.extension?.location.scheme === Schemas.vscodeRemote; const remoteAuthority = this.extension?.location.scheme === Schemas.vscodeRemote ? this.extension.location.authority : undefined; return value .replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => { @@ -386,7 +387,7 @@ export abstract class BaseWebview extends Disposable { scheme: scheme || 'file', path: path, }); - const webviewUri = asWebviewUri(this.id, uri, remoteAuthority).toString(); + const webviewUri = asWebviewUri(this.id, uri, { isRemote, authority: remoteAuthority }).toString(); return `${startQuote}${webviewUri}${endQuote}`; }) .replace(/(["'])(?:vscode-webview-resource):(\/\/[^\s\/'"]+\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => { @@ -394,7 +395,7 @@ export abstract class BaseWebview extends Disposable { scheme: scheme || 'file', path: path, }); - const webviewUri = asWebviewUri(this.id, uri, remoteAuthority).toString(); + const webviewUri = asWebviewUri(this.id, uri, { isRemote, authority: remoteAuthority }).toString(); return `${startQuote}${webviewUri}${endQuote}`; }); } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts index bc06cd1a87c..4d898271f70 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts @@ -492,7 +492,7 @@ export class GettingStartedPage extends EditorPane { if (src.startsWith('https://')) { return `src="${src}"`; } const path = joinPath(base, src); - const transformed = asWebviewUri(this.webviewID, path, undefined).toString(); + const transformed = asWebviewUri(this.webviewID, path).toString(); return `src="${transformed}"`; }); From cd27f1fcb91a40a1faef766b68e4bcec45ec4c38 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Thu, 20 May 2021 16:07:00 -0700 Subject: [PATCH 221/270] update text for loose file button #wt --- src/vs/workbench/services/workspaces/common/workspaceTrust.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 20dd398829b..0d54fab4eb2 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -448,10 +448,10 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa this.workspaceService.getWorkbenchState() !== WorkbenchState.EMPTY ? localize('openLooseFileWorkspaceDetails', "You are trying to open untrusted files in a workspace which is trusted.") : localize('openLooseFileWindowDetails', "You are trying to open untrusted files in a window which is trusted."), - localize('openLooseFileLearnMore', "If you don't trust the authors of these files, we recommend to open them in a new Restricted Mode window as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.") + localize('openLooseFileLearnMore', "If you don't trust the authors of these files, we recommend to open them in Restricted Mode in a new window as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.") ]; - const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Do you trust the authors of these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Restricted Mode Window"), localize('cancel', "Cancel")], { + const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Do you trust the authors of these files?"), [localize('open', "Open"), localize('newWindow', "Open in Restricted Mode"), localize('cancel', "Cancel")], { cancelId: 2, checkbox: { label: localize('openLooseFileWorkspaceCheckbox', "Remember my decision for all workspaces"), From c1b809ef221591205c24648ef1d46ea8c54ddc96 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 20 May 2021 16:15:41 -0700 Subject: [PATCH 222/270] Fix #124307 --- .../welcome/gettingStarted/browser/gettingStarted.css | 8 ++++++++ .../welcome/gettingStarted/browser/gettingStarted.ts | 9 ++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css index 73356b9714d..b1046a3e613 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css @@ -191,6 +191,14 @@ width: 100%; } +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlide .getting-started-category.no-progress { + padding: 3px 12px; +} + +.monaco-workbench .part.editor > .content .gettingStartedContainer .getting-started-category.no-progress .category-progress { + display: none; +} + .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories ul { list-style: none; margin: 0; diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts index 4d898271f70..76d51764542 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts @@ -818,14 +818,17 @@ export class GettingStartedPage extends EditorPane { bar.setAttribute('aria-valuemin', '0'); bar.setAttribute('aria-valuenow', '' + numDone); bar.setAttribute('aria-valuemax', '' + numTotal); - const progress = Math.max((numDone / numTotal) * 100, 3); + const progress = (numDone / numTotal) * 100; bar.style.width = `${progress}%`; + + (element.parentElement as HTMLElement).classList[numDone === 0 ? 'add' : 'remove']('no-progress'); + if (numTotal === numDone) { - bar.title = `All steps complete!`; + bar.title = localize('gettingStarted.allStepsComplete', "All {0} steps complete!", numTotal); } else { - bar.title = `${numDone} of ${numTotal} steps complete`; + bar.title = localize('gettingStarted.someStepsComplete', "{0} of {1} steps complete", numDone, numTotal); } }); } From 5b0fc94e6afd5cf9566c52685547d6cfa6dab744 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Thu, 20 May 2021 16:20:38 -0700 Subject: [PATCH 223/270] fire event when trust is change in empty window #wt --- .../services/workspaces/common/workspaceTrust.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 0d54fab4eb2..b0ae61b0964 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -172,8 +172,11 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork return workspaceUris; } - private async updateWorkspaceTrust(): Promise { - const trusted = this.calculateWorkspaceTrust(); + private async updateWorkspaceTrust(trusted?: boolean): Promise { + if (trusted === undefined) { + trusted = this.calculateWorkspaceTrust(); + } + if (this.isWorkpaceTrusted() === trusted) { return; } // Update workspace trust @@ -299,9 +302,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork async setWorkspaceTrust(trusted: boolean): Promise { // Empty workspace if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { - this._trustState.isTrusted = trusted; - await this.updateWorkspaceTrust(); - + await this.updateWorkspaceTrust(trusted); return; } From f29bd6f18ddefbb7f2cfca3e7e84e6b0a308c95c Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 20 May 2021 16:21:29 -0700 Subject: [PATCH 224/270] Dont autofocus input on ipad Fix #122044 --- src/vs/workbench/contrib/search/browser/searchView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index f20048620be..c15d93d7249 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -902,7 +902,7 @@ export class SearchView extends ViewPane { override focus(): void { super.focus(); - if (this.lastFocusState === 'input' || !this.hasSearchResults()) { + if (!env.isIOS && (this.lastFocusState === 'input' || !this.hasSearchResults())) { const updatedText = this.searchConfig.seedOnFocus ? this.updateTextFromSelection({ allowSearchOnType: false }) : false; this.searchWidget.focus(undefined, undefined, updatedText); } else { From bf4c7042a3a779c6ec05221af056378cd6dc07bb Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 20 May 2021 17:21:28 -0700 Subject: [PATCH 225/270] cell toolbar overlap with notebook toolbar. --- .../notebook/browser/notebookEditorWidget.ts | 15 +++++++++++---- .../notebook/browser/view/notebookCellList.ts | 2 +- .../contrib/notebook/common/notebookOptions.ts | 12 +++++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 3cf28488285..6204eeaf2d6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -369,7 +369,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._updateForNotebookConfiguration(); } - if (e.compactView || e.focusIndicator || e.insertToolbarPosition) { + if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); @@ -663,7 +663,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } // top insert toolbar - const topInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + const topInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); styleSheets.push(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${topInsertToolbarHeight}px }`); styleSheets.push(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { @@ -919,9 +919,16 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor async setModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined): Promise { if (this.viewModel === undefined || !this.viewModel.equal(textModel)) { + const oldTopInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); this._detachModel(); await this._attachModel(textModel, viewState); + const newTopInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); + if (oldTopInsertToolbarHeight !== newTopInsertToolbarHeight) { + this._styleElement?.remove(); + this._createLayoutStyles(); + this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); + } type WorkbenchNotebookOpenClassification = { scheme: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; }; @@ -1219,7 +1226,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); if (this._dimension) { - const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); this._list.layout(this._dimension.height - topInserToolbarHeight, this._dimension.width); } else { this._list.layout(); @@ -1448,7 +1455,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }; } - const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); this._dimension = new DOM.Dimension(dimension.width, dimension.height); DOM.size(this._body, dimension.width, dimension.height - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0)); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index eec47318496..5584ff93977 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -904,7 +904,7 @@ export class NotebookCellList extends WorkbenchList implements ID } getViewScrollBottom() { - const topInsertToolbarHeight = this._viewContext.notebookOptions.computeTopInserToolbarHeight(); + const topInsertToolbarHeight = this._viewContext.notebookOptions.computeTopInserToolbarHeight(this.viewModel?.viewType); return this.getViewScrollTop() + this.view.renderHeight - topInsertToolbarHeight; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index cd20b2d700b..b953078c681 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -330,12 +330,18 @@ export class NotebookOptions { }; } - computeTopInserToolbarHeight(): number { + computeTopInserToolbarHeight(viewType?: string): number { if (this._layoutConfiguration.insertToolbarPosition === 'betweenCells' || this._layoutConfiguration.insertToolbarPosition === 'both') { return SCROLLABLE_ELEMENT_PADDING_TOP; - } else { - return 0; } + + const cellToolbarLocation = this.computeCellToolbarLocation(viewType); + + if (cellToolbarLocation === 'left' || cellToolbarLocation === 'right') { + return SCROLLABLE_ELEMENT_PADDING_TOP; + } + + return 0; } dispose() { From acc07bd9591f8728180da04709e4319801bf241a Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 20 May 2021 16:06:43 -0700 Subject: [PATCH 226/270] Mark properties readonly --- .../notebook/browser/view/renderers/backLayerWebView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 323b6161a34..7b5afb08690 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -431,9 +431,9 @@ export class BackLayerWebView extends Disposable { private _currentKernel?: INotebookKernel; constructor( - public notebookEditor: ICommonNotebookEditor, - public id: string, - public documentUri: URI, + public readonly notebookEditor: ICommonNotebookEditor, + public readonly id: string, + public readonly documentUri: URI, public options: { outputNodePadding: number, outputNodeLeftPadding: number, From 9e04a67d12596c6d74059e3475307a61b56570bd Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 20 May 2021 17:38:03 -0700 Subject: [PATCH 227/270] Switch to use vscode-webview.net as default webview endpoint This moves us off of `vscode-webview-test` to instead use `vscode-webview.net` for loading webview resources. We now also pick this up from the product.json instead of hardcoding it --- product.json | 1 + src/vs/base/common/product.ts | 2 ++ src/vs/platform/product/common/product.ts | 1 + src/vs/workbench/api/common/shared/webview.ts | 10 ++++++---- .../electron-sandbox/iframeWebviewElement.ts | 4 ++-- .../environment/browser/environmentService.ts | 11 +++++------ .../test/browser/api/extHostWebview.test.ts | 13 +++++++------ 7 files changed, 24 insertions(+), 18 deletions(-) diff --git a/product.json b/product.json index a16e67696ef..3f00f8e1468 100644 --- a/product.json +++ b/product.json @@ -22,6 +22,7 @@ "licenseFileName": "LICENSE.txt", "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", "urlProtocol": "code-oss", + "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/{{quality}}/{{commit}}/out/vs/workbench/contrib/webview/browser/pre/", "extensionAllowedProposedApi": [ "ms-vscode.vscode-js-profile-flame", "ms-vscode.vscode-js-profile-table", diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 129b8dec4d9..fb1afd73cef 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -130,6 +130,8 @@ export interface IProductConfiguration { readonly 'configurationSync.store'?: ConfigurationSyncStore; readonly darwinUniversalAssetId?: string; + + readonly webviewContentExternalBaseUrlTemplate: string; } export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean }; diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 28e36704128..b6b67a0b5ac 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -63,6 +63,7 @@ else { reportIssueUrl: 'https://github.com/microsoft/vscode/issues/new', licenseName: 'MIT', licenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt', + webviewContentExternalBaseUrlTemplate: 'https://{{uuid}}.vscode-webview.net/{{quality}}/{{commit}}/out/vs/workbench/contrib/webview/browser/pre/', extensionAllowedProposedApi: [ 'ms-vscode.vscode-js-profile-flame', 'ms-vscode.vscode-js-profile-table', diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index eeecb0ca1f1..de315478caa 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -15,16 +15,18 @@ export interface WebviewInitData { } /** - * Location where we load resources from + * Root from which resources in webviews are loaded. * - * There endpoints can be hardcoded because we never expect to actually hit them. Instead these requests + * This is hardcoded because we never expect to actually hit it. Instead these requests * should always go to a service worker. */ -export const webviewResourceOrigin = (id: string) => `https://${id}.vscode-webview-test.com`; +export const webviewResourceAuthority = 'vscode-webview.net'; + +export const webviewResourceOrigin = (id: string) => `https://${id}.${webviewResourceAuthority}`; export const webviewResourceRoot = (id: string) => `${webviewResourceOrigin(id)}/vscode-resource/{{resource}}`; -export const webviewGenericCspSource = 'https://*.vscode-webview-test.com'; +export const webviewGenericCspSource = `https://*.${webviewResourceAuthority}`; /** * Construct a uri that can load resources inside a webview diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts index 6c66a01bc11..5284437a718 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Schemas } from 'vs/base/common/network'; import { IMenuService } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -69,7 +70,7 @@ export class ElectronIframeWebview extends IFrameWebview { } protected override get webviewContentEndpoint(): string { - const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id); + const endpoint = `${Schemas.vscodeWebview}://${this.id}`; if (endpoint[endpoint.length - 1] === '/') { return endpoint.slice(0, endpoint.length - 1); } @@ -79,5 +80,4 @@ export class ElectronIframeWebview extends IFrameWebview { protected override async doPostMessage(channel: string, data?: any): Promise { this.element?.contentWindow!.postMessage({ channel, args: data }, '*'); } - } diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index a4046f66004..65ec7e95496 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -226,14 +226,13 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get disableExtensions() { return this.payload?.get('disableExtensions') === 'true'; } - private get webviewEndpoint(): string { - // TODO@matt: get fallback from product service - return this.options.webviewEndpoint || 'https://{{uuid}}.vscode-webview-test.com/{{commit}}'; - } - @memoize get webviewExternalEndpoint(): string { - return (this.webviewEndpoint).replace('{{commit}}', this.productService.commit || '23a2409675bc1bde94f3532bc7c5826a6e99e4b6'); + const endpoint = this.options.webviewEndpoint || this.productService.webviewContentExternalBaseUrlTemplate; + console.log(endpoint); + return endpoint + .replace('{{commit}}', this.productService.commit || '97740a7d253650f9f186c211de5247e2577ce9f7') + .replace('{{quality}}', this.productService.quality || 'insider'); } @memoize diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index 4dd59f11662..12ebcc51e9a 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -15,6 +15,7 @@ import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDep import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; +import { webviewResourceAuthority } from 'vs/workbench/api/common/shared/webview'; import { EditorGroupColumn } from 'vs/workbench/common/editor'; import type * as vscode from 'vscode'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; @@ -80,31 +81,31 @@ suite('ExtHostWebview', () => { assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), - 'vscode-webview-test.com/vscode-resource/file//Users/codey/file.html', + `${webviewResourceAuthority}/vscode-resource/file//Users/codey/file.html`, 'Unix basic' ); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), - 'vscode-webview-test.com/vscode-resource/file//Users/codey/file.html#frag', + `${webviewResourceAuthority}/vscode-resource/file//Users/codey/file.html#frag`, 'Unix should preserve fragment' ); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), - 'vscode-webview-test.com/vscode-resource/file//Users/codey/f%20ile.html', + `${webviewResourceAuthority}/vscode-resource/file//Users/codey/f%20ile.html`, 'Unix with encoding' ); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), - 'vscode-webview-test.com/vscode-resource/file/localhost/Users/codey/file.html', + `${webviewResourceAuthority}/vscode-resource/file/localhost/Users/codey/file.html`, 'Unix should preserve authority' ); assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), - 'vscode-webview-test.com/vscode-resource/file//c%3A/codey/file.txt', + `${webviewResourceAuthority}/vscode-resource/file//c%3A/codey/file.txt`, 'Windows C drive' ); }); @@ -114,7 +115,7 @@ suite('ExtHostWebview', () => { assert.strictEqual( stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), - 'vscode-webview-test.com/vscode-resource/vscode-remote/remote/Users/codey/file.html', + `${webviewResourceAuthority}/vscode-resource/vscode-remote/remote/Users/codey/file.html`, 'Unix basic' ); }); From 3e5faf69c14e0e6a5ff36c59bbb0c4eaf872da67 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 20 May 2021 17:39:34 -0700 Subject: [PATCH 228/270] Remove console.log --- .../workbench/services/environment/browser/environmentService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 65ec7e95496..a6b832cf35b 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -229,7 +229,6 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get webviewExternalEndpoint(): string { const endpoint = this.options.webviewEndpoint || this.productService.webviewContentExternalBaseUrlTemplate; - console.log(endpoint); return endpoint .replace('{{commit}}', this.productService.commit || '97740a7d253650f9f186c211de5247e2577ce9f7') .replace('{{quality}}', this.productService.quality || 'insider'); From 2f2b6b528d0062b81d7b3e16c88115827b84cfca Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 20 May 2021 18:26:47 -0700 Subject: [PATCH 229/270] testing: show duration for test results, handle state computation better --- .../hierarchalByLocation.ts | 51 +++++++--- .../browser/explorerProjections/index.ts | 20 ++++ .../testing/browser/testingExplorerView.ts | 36 +++++++- .../testing/common/getComputedState.ts | 92 ++++++++++++++----- .../contrib/testing/common/testCollection.ts | 2 + .../contrib/testing/common/testResult.ts | 3 +- 6 files changed, 162 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index 4e7eda0020c..31f26f29aa9 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -4,25 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { mapFind } from 'vs/base/common/arrays'; import { Emitter } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; +import { Iterable } from 'vs/base/common/iterator'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { TestItemTreeElement, ITestTreeProjection, IActionableTestTreeElement, TestExplorerTreeElement, TestTreeErrorMessage, isActionableTestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; -import { ByLocationTestItemElement, ByLocationFolderElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; +import { ByLocationFolderElement, ByLocationTestItemElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; +import { IActionableTestTreeElement, isActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; -import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; +import { IComputedStateAndDurationAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; import { InternalTestItem, TestDiffOpType, TestItemExpandState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; -import { mapFind } from 'vs/base/common/arrays'; -import { Iterable } from 'vs/base/common/iterator'; -const computedStateAccessor: IComputedStateAccessor = { +const computedStateAccessor: IComputedStateAndDurationAccessor = { getOwnState: i => i instanceof TestItemTreeElement ? i.ownState : TestResultState.Unset, getCurrentComputedState: i => i.state, setComputedState: (i, s) => i.state = s, + + getCurrentComputedDuration: i => i.duration, + getOwnDuration: i => i instanceof TestItemTreeElement ? i.ownDuration : undefined, + setComputedDuration: (i, d) => i.duration = d, + getChildren: i => Iterable.filter(i.children.values(), isActionableTestTreeElement), *getParents(i) { for (let parent = i.parent; parent; parent = parent.parent) { @@ -72,7 +77,13 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes for (const { items } of this.folders.values()) { for (const inTree of [...items.values()].sort((a, b) => b.depth - a.depth)) { const lookup = this.results.getStateById(inTree.test.item.extId)?.[1]; - const computed = lookup?.computedState ?? TestResultState.Unset; + let computed = TestResultState.Unset; + let ownDuration: number | undefined; + let updated = false; + if (lookup) { + computed = lookup.computedState; + ownDuration = lookup.ownDuration; + } if (lookup) { inTree.ownState = lookup.ownComputedState; @@ -80,6 +91,15 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes if (computed !== inTree.state) { inTree.state = computed; + updated = true; + } + + if (ownDuration !== inTree.ownDuration) { + inTree.ownDuration = ownDuration; + updated = true; + } + + if (updated) { this.addUpdated(inTree); } } @@ -90,12 +110,14 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes // when test states change, reflect in the tree // todo: optimize this to avoid needing to iterate - this._register(results.onTestChanged(({ item: result }) => { + this._register(results.onTestChanged(({ item: result, reason }) => { for (const { items } of this.folders.values()) { const item = items.get(result.item.extId); if (item) { item.retired = result.retired; - refreshComputedState(computedStateAccessor, item, this.addUpdated, result.computedState); + item.ownState = result.ownComputedState; + item.ownDuration = result.ownDuration; + refreshComputedState(computedStateAccessor, item).forEach(this.addUpdated); this.addUpdated(item); this.updateEmitter.fire(); } @@ -265,8 +287,13 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes }; protected unstoreItem(items: Map, treeElement: ByLocationTestItemElement) { - treeElement.parent.children.delete(treeElement); + const parent = treeElement.parent; + parent.children.delete(treeElement); items.delete(treeElement.test.item.extId); + if (parent instanceof ByLocationTestItemElement) { + refreshComputedState(computedStateAccessor, parent).forEach(this.addUpdated); + } + return treeElement.children; } @@ -282,7 +309,9 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes const prevState = this.results.getStateById(treeElement.test.item.extId)?.[1]; if (prevState) { treeElement.retired = prevState.retired; - refreshComputedState(computedStateAccessor, treeElement, this.addUpdated, prevState.computedState); + treeElement.ownState = prevState.computedState; + treeElement.ownDuration = prevState.ownDuration; + refreshComputedState(computedStateAccessor, treeElement).forEach(this.addUpdated); } } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index 2b3d2d871d1..eb66e315e25 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -87,6 +87,11 @@ export interface IActionableTestTreeElement { */ state: TestResultState; + /** + * Time it took this test/item to run. + */ + duration: number | undefined; + /** * Label for the item. */ @@ -118,6 +123,11 @@ export class TestTreeWorkspaceFolder implements IActionableTestTreeElement { */ public readonly depth = 0; + /** + * Time it took this test/item to run. + */ + public duration: number | undefined; + /** * @inheritdoc */ @@ -207,6 +217,16 @@ export class TestItemTreeElement implements IActionableTestTreeElement { */ public ownState = TestResultState.Unset; + /** + * Own, non-computed duration. + */ + public ownDuration: number | undefined; + + /** + * Time it took this test/item to run. + */ + public duration: number | undefined; + /** * @inheritdoc */ diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index d289bc892d6..05fa88cabb3 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -821,11 +821,20 @@ const getLabelForTestTreeElement = (element: IActionableTestTreeElement) => { comment: ['label then the unit tests state, for example "Addition Tests (Running)"'], }, '{0} ({1})', element.label, testStateNames[element.state]); - if (element instanceof TestItemTreeElement && element.retired) { - label = localize({ - key: 'testing.treeElementLabelOutdated', - comment: ['{0} is the original label in testing.treeElementLabel'], - }, '{0}, outdated result', label, testStateNames[element.state]); + if (element instanceof TestItemTreeElement) { + if (element.duration !== undefined) { + label = localize({ + key: 'testing.treeElementLabelDuration', + comment: ['{0} is the original label in testing.treeElementLabel, {1} is a duration'], + }, '{0}, in {1}', label, formatDuration(element.duration)); + } + + if (element.retired) { + label = localize({ + key: 'testing.treeElementLabelOutdated', + comment: ['{0} is the original label in testing.treeElementLabel'], + }, '{0}, outdated result', label, testStateNames[element.state]); + } } return label; @@ -1036,10 +1045,27 @@ class TestItemRenderer extends ActionableItemTemplateData { options.title = getLabelForTestTreeElement(node.element); options.fileKind = FileKind.FILE; label.description = node.element.description || undefined; + + if (node.element.duration) { + label.description = label.description + ? `${label.description}: ${formatDuration(node.element.duration)}` + : formatDuration(node.element.duration); + } + data.label.setResource(label, options); } } +const formatDuration = (ms: number) => { + if (ms < 10) { + return `${ms.toPrecision(2)}ms`; + } else if (ms < 1000) { + return `${ms.toPrecision(3)}ms`; + } else { + return `${(ms / 1000).toPrecision(3)}s`; + } +}; + class WorkspaceFolderRenderer extends ActionableItemTemplateData { public static readonly ID = 'workspaceFolder'; diff --git a/src/vs/workbench/contrib/testing/common/getComputedState.ts b/src/vs/workbench/contrib/testing/common/getComputedState.ts index e7400f3f700..34042e3a71a 100644 --- a/src/vs/workbench/contrib/testing/common/getComputedState.ts +++ b/src/vs/workbench/contrib/testing/common/getComputedState.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Iterable } from 'vs/base/common/iterator'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates'; @@ -17,6 +18,14 @@ export interface IComputedStateAccessor { getParents(item: T): Iterable; } +export interface IComputedStateAndDurationAccessor extends IComputedStateAccessor { + getOwnDuration(item: T): number | undefined; + getCurrentComputedDuration(item: T): number | undefined; + setComputedDuration(item: T, duration: number): void; +} + +export const isDurationAccessor = (accessor: IComputedStateAccessor): accessor is IComputedStateAndDurationAccessor => 'getOwnDuration' in accessor; + /** * Gets the computed state for the node. * @param force whether to refresh the computed state for this node, even @@ -36,49 +45,82 @@ export const getComputedState = (accessor: IComputedStateAccessor, node: T return computed; }; + +export const getComputedDuration = (accessor: IComputedStateAndDurationAccessor, node: T, force = false): number => { + let computed = accessor.getCurrentComputedDuration(node); + if (computed === undefined || force) { + const own = accessor.getOwnDuration(node); + if (own !== undefined) { + computed = own; + } else { + computed = 0; + for (const child of accessor.getChildren(node)) { + computed += getComputedDuration(accessor, child); + } + } + + accessor.setComputedDuration(node, computed); + } + + return computed; +}; + /** * Refreshes the computed state for the node and its parents. Any changes * elements cause `addUpdated` to be called. */ - export const refreshComputedState = ( accessor: IComputedStateAccessor, node: T, - addUpdated: (node: T) => void, explicitNewComputedState?: TestResultState, ) => { const oldState = accessor.getCurrentComputedState(node); const oldPriority = statePriority[oldState]; const newState = explicitNewComputedState ?? getComputedState(accessor, node, true); const newPriority = statePriority[newState]; - if (newPriority === oldPriority) { - return; + const toUpdate = new Set(); + + if (newPriority !== oldPriority) { + accessor.setComputedState(node, newState); + toUpdate.add(node); + + if (newPriority > oldPriority) { + // Update all parents to ensure they're at least this priority. + for (const parent of accessor.getParents(node)) { + const prev = accessor.getCurrentComputedState(parent); + if (prev !== undefined && statePriority[prev] >= newPriority) { + break; + } + + accessor.setComputedState(parent, newState); + toUpdate.add(parent); + } + } else if (newPriority < oldPriority) { + // Re-render all parents of this node whose computed priority might have come from this node + for (const parent of accessor.getParents(node)) { + const prev = accessor.getCurrentComputedState(parent); + if (prev === undefined || statePriority[prev] > oldPriority) { + break; + } + + accessor.setComputedState(parent, getComputedState(accessor, parent, true)); + toUpdate.add(parent); + } + } } - accessor.setComputedState(node, newState); - addUpdated(node); - - if (newPriority > oldPriority) { - // Update all parents to ensure they're at least this priority. - for (const parent of accessor.getParents(node)) { - const prev = accessor.getCurrentComputedState(parent); - if (prev !== undefined && statePriority[prev] >= newPriority) { + if (isDurationAccessor(accessor)) { + for (const parent of Iterable.concat(Iterable.single(node), accessor.getParents(node))) { + const oldDuration = accessor.getCurrentComputedDuration(node); + const newDuration = getComputedDuration(accessor, node, true); + if (oldDuration === newDuration) { break; } - accessor.setComputedState(parent, newState); - addUpdated(parent); - } - } else if (newPriority < oldPriority) { - // Re-render all parents of this node whose computed priority might have come from this node - for (const parent of accessor.getParents(node)) { - const prev = accessor.getCurrentComputedState(parent); - if (prev === undefined || statePriority[prev] > oldPriority) { - break; - } - - accessor.setComputedState(parent, getComputedState(accessor, parent, true)); - addUpdated(parent); + accessor.setComputedDuration(parent, newState); + toUpdate.add(parent); } } + + return toUpdate; }; diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index e08df28a267..55c05ea8fd1 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -148,6 +148,8 @@ export interface TestResultItem { computedState: TestResultState; /** True if the test is outdated */ retired: boolean; + /** Max duration of the item's tasks (if run directly) */ + ownDuration?: number; /** True if the test was directly requested by the run (is not a child or parent) */ direct?: boolean; } diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index 2bd41541af5..bfb75fcd54a 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -344,6 +344,7 @@ export class LiveTestResult implements ITestResult { const index = this.mustGetTaskIndex(taskId); if (duration !== undefined) { entry.tasks[index].duration = duration; + entry.ownDuration = Math.max(entry.ownDuration || 0, duration); } this.fireUpdateAndRefresh(entry, index, state); @@ -463,7 +464,7 @@ export class LiveTestResult implements ITestResult { entry.ownComputedState = newOwnComputed; this.counts[previousOwnComputed]--; this.counts[newOwnComputed]++; - refreshComputedState(this.computedStateAccessor, entry, t => + refreshComputedState(this.computedStateAccessor, entry).forEach(t => this.changeEmitter.fire( t === entry ? { item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, previous: previousOwnComputed } From bfb822e4cf0a545f8c0dfc6fe59d72112d11dd18 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 20 May 2021 21:15:16 -0700 Subject: [PATCH 230/270] fix #124284 --- src/vs/workbench/contrib/terminal/browser/terminalGroup.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 98b09532c72..b7b915f93c9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -43,7 +43,7 @@ class SplitPaneContainer extends Disposable { this._splitViewDisposables.add(this._splitView.onDidSashReset(() => this._splitView.distributeViewSizes())); } - split(instance: ITerminalInstance, index: number = this._children.length): void { + split(instance: ITerminalInstance, index: number): void { this._addChild(instance, index); } @@ -283,7 +283,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._initInstanceListeners(instance); if (this._splitPaneContainer) { - this._splitPaneContainer!.split(instance); + this._splitPaneContainer!.split(instance, this._activeInstanceIndex + 1); } instance.setVisible(this._isVisible); @@ -440,7 +440,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { const orientation = this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; const newLocal = this._instantiationService.createInstance(SplitPaneContainer, this._groupElement, orientation); this._splitPaneContainer = newLocal; - this.terminalInstances.forEach(instance => this._splitPaneContainer!.split(instance)); + this.terminalInstances.forEach(instance => this._splitPaneContainer!.split(instance, this._activeInstanceIndex + 1)); } this.setVisible(this._isVisible); } From 4fd4e10e99eff9eb77408b71c7f58b7ed7458ee5 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 20 May 2021 21:58:08 -0700 Subject: [PATCH 231/270] Fix #124146? --- .../contrib/search/browser/search.contribution.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 61197043ff4..e54b8b2a8fb 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -17,7 +17,7 @@ import { Action2, ICommandAction, MenuId, MenuRegistry, registerAction2, SyncAct import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { ContextKeyEqualsExpr, ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyRegexExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -636,6 +636,8 @@ const viewDescriptor: IViewDescriptor = { mnemonicTitle: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search"), keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F, + // Yes, this is weird. See #116188, #115556, #115511, and now #124146, for examples of what can go wrong here. + when: ContextKeyRegexExpr.create('neverMatch', /doesNotMatch/) }, order: 1 } @@ -709,8 +711,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ ] }, id: Constants.FindInFilesActionId, - // Give more weightage to this keybinding than of `View: Show Search` keybinding. See #116188, #115556, #115511 - weight: KeybindingWeight.WorkbenchContrib + 1, + weight: KeybindingWeight.WorkbenchContrib, when: null, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_F, handler: FindInFilesCommand From 89c8f9198846a918809668756bd6e24a0570b2b5 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 20 May 2021 22:20:48 -0700 Subject: [PATCH 232/270] Add smoke test for #124146 --- test/automation/src/search.ts | 5 +++++ test/smoke/src/areas/search/search.test.ts | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/test/automation/src/search.ts b/test/automation/src/search.ts index ec0078d11ba..d5637906834 100644 --- a/test/automation/src/search.ts +++ b/test/automation/src/search.ts @@ -43,6 +43,11 @@ export class Search extends Viewlet { await this.waitForInputFocus(INPUT); } + async getSearchTooltip(): Promise { + const icon = await this.code.waitForElement(`.activitybar .action-label.codicon.codicon-search-view-icon`); + return icon.attributes['title']; + } + async searchFor(text: string): Promise { await this.waitForInputFocus(INPUT); await this.code.waitForSetValue(INPUT, text); diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 7b864b8bc74..8bc11e6d831 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -15,6 +15,15 @@ export function setup() { cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder }); }); + // https://github.com/microsoft/vscode/issues/124146 + it('has a tooltp with a keybinding', async function () { + const app = this.app as Application; + const tooltip: string = await app.workbench.search.getSearchTooltip(); + if (!/Search \([^F]+F\)/.test(tooltip)) { + throw Error(`Expected search tooltip to contain keybinding but got ${tooltip}`); + } + }); + it('searches for body & checks for correct result number', async function () { const app = this.app as Application; await app.workbench.search.openSearchViewlet(); From e66c62f38a2c0f395de09a992a13b6acc4c99eee Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 20 May 2021 22:21:19 -0700 Subject: [PATCH 233/270] Generalize smoke text --- test/smoke/src/areas/search/search.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 8bc11e6d831..72ec498f988 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -19,7 +19,7 @@ export function setup() { it('has a tooltp with a keybinding', async function () { const app = this.app as Application; const tooltip: string = await app.workbench.search.getSearchTooltip(); - if (!/Search \([^F]+F\)/.test(tooltip)) { + if (!/Search \(.+\)/.test(tooltip)) { throw Error(`Expected search tooltip to contain keybinding but got ${tooltip}`); } }); From d0c5675f6f29061887c13b6d423f65aa1f673fce Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 07:38:01 +0200 Subject: [PATCH 234/270] editors - less group.openEditor usage //cc @lramos15 --- .../browser/parts/editor/editorDropTarget.ts | 2 +- .../searchEditor/browser/searchEditorActions.ts | 7 +++---- .../browser/gettingStartedInput.ts | 3 ++- .../browser/gettingStartedService.ts | 16 ++++++++-------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index b3a6d19de50..dd119d85c00 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -343,7 +343,7 @@ class DropOverlay extends Themable { targetGroup = ensureTargetGroup(); } - await targetGroup.openEditor(untitledTextEditor); + await this.editorService.openEditor(untitledTextEditor, undefined, targetGroup.id); } }; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 41c415ed5ce..4a6ca12b468 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Schemas } from 'vs/base/common/network'; -import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; +import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/searchEditor'; import { ICodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { EditorOverride } from 'vs/platform/editor/common/editor'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -23,7 +24,6 @@ import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/ import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; @@ -99,7 +99,6 @@ export async function openSearchEditor(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); - const editorGroupsService = accessor.get(IEditorGroupsService); const telemetryService = accessor.get(ITelemetryService); const instantiationService = accessor.get(IInstantiationService); const configurationService = accessor.get(IConfigurationService); @@ -148,7 +147,7 @@ export const openNewSearchEditor = let editor: SearchEditor; if (existing && args.location === 'reuse') { const input = existing.editor as SearchEditorInput; - editor = assertIsDefined(await assertIsDefined(editorGroupsService.getGroup(existing.groupId)).openEditor(input)) as SearchEditor; + editor = (await editorService.openEditor(input, { override: EditorOverride.DISABLED }, existing.groupId)) as SearchEditor; if (selected) { editor.setQuery(selected); } else { editor.selectQuery(); } editor.setSearchConfig(args); diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts index a6639f10062..6aa2d4ec4eb 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput.ts @@ -14,13 +14,14 @@ export const gettingStartedInputTypeId = 'workbench.editors.gettingStartedInput' export class GettingStartedInput extends EditorInput { static readonly ID = gettingStartedInputTypeId; + static readonly RESOURCE = URI.from({ scheme: Schemas.walkThrough, authority: 'vscode_getting_started_page' }); override get typeId(): string { return GettingStartedInput.ID; } get resource(): URI | undefined { - return URI.from({ scheme: Schemas.walkThrough, authority: 'vscode_getting_started_page' }); + return GettingStartedInput.RESOURCE; } override matches(other: unknown) { diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts index b49d8ad9148..b8cefbe30df 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts @@ -33,6 +33,7 @@ import { walkthroughsExtensionPoint } from 'vs/workbench/contrib/welcome/getting import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { dirname } from 'vs/base/common/path'; import { coalesce, flatten } from 'vs/base/common/arrays'; +import { EditorOverride } from 'vs/platform/editor/common/editor'; export const IGettingStartedService = createDecorator('gettingStartedService'); @@ -438,14 +439,13 @@ export class GettingStartedService extends Disposable implements IGettingStarted } // Otherwise, try to find a getting started input somewhere with no selected walkthrough, and open it to this one. - for (const group of this.editorGroupsService.groups) { - for (const editor of group.editors) { - if (editor instanceof GettingStartedInput) { - if (!editor.selectedCategory) { - editor.selectedCategory = sectionToOpen; - group.openEditor(editor, { revealIfOpened: true }); - return; - } + const result = this.editorService.findEditors({ typeId: GettingStartedInput.ID, resource: GettingStartedInput.RESOURCE }); + for (const { editor, groupId } of result) { + if (editor instanceof GettingStartedInput) { + if (!editor.selectedCategory) { + editor.selectedCategory = sectionToOpen; + this.editorService.openEditor(editor, { revealIfOpened: true, override: EditorOverride.DISABLED }, groupId); + return; } } } From 633ea8570887f2d9225368d386ab1184d76cc127 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 07:45:33 +0200 Subject: [PATCH 235/270] notebook working copy - some cleanup before refactorings --- .../notebook/browser/notebook.contribution.ts | 11 +++--- .../notebook/common/notebookEditorModel.ts | 37 +++++++++---------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 34d93102f1f..c2fe1b21212 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -53,6 +53,11 @@ import { EditorOverride } from 'vs/platform/editor/common/editor'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl'; +import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; // Editor Contribution import 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard'; @@ -73,17 +78,11 @@ import 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperati import 'vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown'; import 'vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout'; - // Diff Editor Contribution import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions'; // Output renderers registration import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl'; -import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; /*--------------------------------------------------------------------------------------------- */ diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 5fc06a8afdb..c07df5e1bb9 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -23,14 +23,14 @@ import { TaskSequentializer } from 'vs/base/common/async'; import { bufferToStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { assertType } from 'vs/base/common/types'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { FileWorkingCopyState, IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory, IResolvedFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { filter } from 'vs/base/common/objects'; import { IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; -import { IResolvedUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; //#region --- complex content provider @@ -426,7 +426,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE readonly onDidChangeOrphaned: Event = this._onDidChangeOrphaned.event; readonly onDidChangeReadonly: Event = this._onDidChangeReadonly.event; - private _workingCopy?: IResolvedUntitledFileWorkingCopy | IResolvedFileWorkingCopy; + private _workingCopy?: IFileWorkingCopy | IUntitledFileWorkingCopy; private readonly _workingCopyListeners = new DisposableStore(); constructor( @@ -450,7 +450,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } get notebook(): NotebookTextModel | undefined { - return this._workingCopy?.model.notebookModel; + return this._workingCopy?.model?.notebookModel; } override isResolved(): this is IResolvedNotebookEditorModel { @@ -462,14 +462,14 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } isOrphaned(): boolean { - return !!this._workingCopy && SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(FileWorkingCopyState.ORPHAN); + return SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(FileWorkingCopyState.ORPHAN); } isReadonly(): boolean { - if (this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + if (SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy)) { + return this._workingCopy.isReadonly(); + } else if (this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { return true; - } else if (this._workingCopy && SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy)) { - return this._workingCopy?.isReadonly(); } else { return false; } @@ -489,20 +489,18 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE if (!this._workingCopy) { if (this.resource.scheme === Schemas.untitled) { - const workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource }); - this._workingCopy = >workingCopy; + this._workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource }); } else { - const workingCopy = await this._workingCopyManager.resolve(this.resource, { forceReadFromFile: options?.forceReadFromFile }); - this._workingCopyListeners.add(workingCopy.onDidSave(() => this._onDidSave.fire())); - this._workingCopyListeners.add(workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); - this._workingCopyListeners.add(workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire())); - this._workingCopy = >workingCopy; + this._workingCopy = await this._workingCopyManager.resolve(this.resource, { forceReadFromFile: options?.forceReadFromFile }); + this._workingCopyListeners.add(this._workingCopy.onDidSave(() => this._onDidSave.fire())); + this._workingCopyListeners.add(this._workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); + this._workingCopyListeners.add(this._workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire())); } this._workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(), undefined, this._workingCopyListeners); this._workingCopyListeners.add(this._workingCopy.onWillDispose(() => { this._workingCopyListeners.clear(); - this._workingCopy?.model.dispose(); + this._workingCopy?.model?.dispose(); })); } @@ -515,14 +513,13 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE if (!newWorkingCopy) { return undefined; } - assertType(newWorkingCopy.isResolved()); // this is a little hacky because we leave the new working copy alone. BUT // the newly created editor input will pick it up and claim ownership of it. return this._instantiationService.createInstance(NotebookEditorInput, newWorkingCopy.resource, this.viewType, {}); } - private static _isFileWorkingCopy(candidate: IResolvedUntitledFileWorkingCopy | IResolvedFileWorkingCopy): candidate is IResolvedFileWorkingCopy { - return typeof (>candidate).hasState === 'function'; + private static _isFileWorkingCopy(candidate?: unknown): candidate is IFileWorkingCopy { + return candidate instanceof FileWorkingCopy; } } @@ -617,7 +614,7 @@ export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel, IUnt } } -export class NotebookFileWorkingCopyModelFactory implements IFileWorkingCopyModelFactory{ +export class NotebookFileWorkingCopyModelFactory implements IFileWorkingCopyModelFactory, IUntitledFileWorkingCopyModelFactory{ constructor( private readonly _viewType: string, From 78fbc51ad756e4b36a1a2498df5272a8f709edf1 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 20 May 2021 22:56:19 -0700 Subject: [PATCH 236/270] Close #124325 --- .../contrib/url/browser/trustedDomainsValidator.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index 414c0799974..c3e8a1eedde 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -22,6 +22,7 @@ import { IdleValue } from 'vs/base/common/async'; import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { testUrlMatchesGlob } from 'vs/workbench/contrib/url/common/urlGlob'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; type TrustedDomainsDialogActionClassification = { action: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -44,6 +45,7 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly _workspaceTrustService: IWorkspaceTrustManagementService, ) { this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); @@ -69,6 +71,10 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { return true; } + if (this._workspaceTrustService.isWorkpaceTrusted()) { + return true; + } + const originalResource = resource; if (typeof resource === 'string') { resource = URI.parse(resource); From 0e58bef15cd6fb223bd8723d7185a69871a0c569 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 08:23:51 +0200 Subject: [PATCH 237/270] file working copy => stored file working copy --- .../notebook/common/notebookEditorModel.ts | 18 +- .../common/fileWorkingCopyManager2.ts | 110 +++++----- ...orkingCopy.ts => storedFileWorkingCopy.ts} | 204 +++++++++--------- ...ger.ts => storedFileWorkingCopyManager.ts} | 115 +++++----- .../browser/fileWorkingCopyManager2.test.ts | 26 +-- ....test.ts => storedFileWorkingCopy.test.ts} | 122 +++++------ ...s => storedFileWorkingCopyManager.test.ts} | 25 +-- .../untitledFileWorkingCopyManager.test.ts | 11 +- 8 files changed, 319 insertions(+), 312 deletions(-) rename src/vs/workbench/services/workingCopy/common/{fileWorkingCopy.ts => storedFileWorkingCopy.ts} (81%) rename src/vs/workbench/services/workingCopy/common/{fileWorkingCopyManager.ts => storedFileWorkingCopyManager.ts} (81%) rename src/vs/workbench/services/workingCopy/test/browser/{fileWorkingCopy.test.ts => storedFileWorkingCopy.test.ts} (78%) rename src/vs/workbench/services/workingCopy/test/browser/{fileWorkingCopyManager.test.ts => storedFileWorkingCopyManager.test.ts} (94%) diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index c07df5e1bb9..5cba38973fb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -23,7 +23,7 @@ import { TaskSequentializer } from 'vs/base/common/async'; import { bufferToStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { assertType } from 'vs/base/common/types'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; @@ -426,7 +426,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE readonly onDidChangeOrphaned: Event = this._onDidChangeOrphaned.event; readonly onDidChangeReadonly: Event = this._onDidChangeReadonly.event; - private _workingCopy?: IFileWorkingCopy | IUntitledFileWorkingCopy; + private _workingCopy?: IStoredFileWorkingCopy | IUntitledFileWorkingCopy; private readonly _workingCopyListeners = new DisposableStore(); constructor( @@ -462,11 +462,11 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } isOrphaned(): boolean { - return SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(FileWorkingCopyState.ORPHAN); + return SimpleNotebookEditorModel._isStordFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN); } isReadonly(): boolean { - if (SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy)) { + if (SimpleNotebookEditorModel._isStordFileWorkingCopy(this._workingCopy)) { return this._workingCopy.isReadonly(); } else if (this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { return true; @@ -518,14 +518,14 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE return this._instantiationService.createInstance(NotebookEditorInput, newWorkingCopy.resource, this.viewType, {}); } - private static _isFileWorkingCopy(candidate?: unknown): candidate is IFileWorkingCopy { - return candidate instanceof FileWorkingCopy; + private static _isStordFileWorkingCopy(candidate?: unknown): candidate is IStoredFileWorkingCopy { + return candidate instanceof StoredFileWorkingCopy; } } -export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel, IUntitledFileWorkingCopyModel { +export class NotebookFileWorkingCopyModel implements IStoredFileWorkingCopyModel, IUntitledFileWorkingCopyModel { - private readonly _onDidChangeContent = new Emitter(); + private readonly _onDidChangeContent = new Emitter(); private readonly _changeListener: IDisposable; readonly onDidChangeContent = this._onDidChangeContent.event; @@ -614,7 +614,7 @@ export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel, IUnt } } -export class NotebookFileWorkingCopyModelFactory implements IFileWorkingCopyModelFactory, IUntitledFileWorkingCopyModelFactory{ +export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCopyModelFactory, IUntitledFileWorkingCopyModelFactory{ constructor( private readonly _viewType: string, diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts index 50e8b41d4da..8c256e0246a 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts @@ -17,8 +17,8 @@ import { ISaveOptions } from 'vs/workbench/common/editor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; -import { FileWorkingCopyManager, IFileWorkingCopyManager, IFileWorkingCopyManagerResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; +import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; +import { StoredFileWorkingCopyManager, IStoredFileWorkingCopyManager, IStoredFileWorkingCopyManagerResolveOptions } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; import { INewOrExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCopyWithAssociatedResourceOptions, IUntitledFileWorkingCopyManager, UntitledFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @@ -37,12 +37,12 @@ import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/com import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export interface IFileWorkingCopyManager2 extends IBaseFileWorkingCopyManager> { +export interface IFileWorkingCopyManager2 extends IBaseFileWorkingCopyManager> { /** - * Provides access to the manager for titled file working copies. + * Provides access to the manager for stored file working copies. */ - readonly files: IFileWorkingCopyManager; + readonly stored: IStoredFileWorkingCopyManager; /** * Provides access to the manager for untitled file working copies. @@ -50,22 +50,24 @@ export interface IFileWorkingCopyManager2; /** - * Allows to resolve a file working copy. If the manager already knows - * about a file working copy with the same `URI`, it will return that - * existing file working copy. There will never be more than one - * file working copy per `URI` until the file working copy is disposed. + * Allows to resolve a stored file working copy. If the manager already knows + * about a stored file working copy with the same `URI`, it will return that + * existing stored file working copy. There will never be more than one + * stored file working copy per `URI` until the stored file working copy is + * disposed. * - * Use the `IFileWorkingCopyResolveOptions.reload` option to control the - * behaviour for when a file working copy was previously already resolved + * Use the `IStoredFileWorkingCopyResolveOptions.reload` option to control the + * behaviour for when a stored file working copy was previously already resolved * with regards to resolving it again from the underlying file resource * or not. * * Note: Callers must `dispose` the working copy when no longer needed. * - * @param resource used as unique identifier of the file working copy in + * @param resource used as unique identifier of the stored file working copy in * case one is already known for this `URI`. + * @param options */ - resolve(resource: URI, options?: IFileWorkingCopyManagerResolveOptions): Promise>; + resolve(resource: URI, options?: IStoredFileWorkingCopyManagerResolveOptions): Promise>; /** * Create a new untitled file working copy with optional initial contents. @@ -111,11 +113,11 @@ export interface IFileWorkingCopyManager2 | undefined>; - saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; + saveAs(source: URI, target: URI, options?: ISaveOptions): Promise | undefined>; + saveAs(source: URI, target: undefined, options?: IFileWorkingCopySaveAsOptions): Promise | undefined>; } export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { @@ -127,17 +129,17 @@ export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { suggestedTarget?: URI; } -export class FileWorkingCopyManager2 extends Disposable implements IFileWorkingCopyManager2 { +export class FileWorkingCopyManager2 extends Disposable implements IFileWorkingCopyManager2 { - readonly onDidCreate: Event>; + readonly onDidCreate: Event>; - readonly files: IFileWorkingCopyManager; + readonly stored: IStoredFileWorkingCopyManager; readonly untitled: IUntitledFileWorkingCopyManager; constructor( private readonly workingCopyTypeId: string, - private readonly fileModelFactory: IFileWorkingCopyModelFactory, - private readonly untitledFileModelFactory: IUntitledFileWorkingCopyModelFactory, + private readonly storedWorkingCopyModelFactory: IStoredFileWorkingCopyModelFactory, + private readonly untitledWorkingCopyModelFactory: IUntitledFileWorkingCopyModelFactory, @IFileService private readonly fileService: IFileService, @ILifecycleService lifecycleService: ILifecycleService, @ILabelService labelService: ILabelService, @@ -159,19 +161,19 @@ export class FileWorkingCopyManager2 { const result = await this.saveAs(workingCopy.resource, undefined, options); @@ -181,17 +183,17 @@ export class FileWorkingCopyManager2>(this.files.onDidCreate, this.untitled.onDidCreate); + this.onDidCreate = Event.any>(this.stored.onDidCreate, this.untitled.onDidCreate); } //#region get / get all - get workingCopies(): (IUntitledFileWorkingCopy | IFileWorkingCopy)[] { - return [...this.files.workingCopies, ...this.untitled.workingCopies]; + get workingCopies(): (IUntitledFileWorkingCopy | IStoredFileWorkingCopy)[] { + return [...this.stored.workingCopies, ...this.untitled.workingCopies]; } - get(resource: URI): IUntitledFileWorkingCopy | IFileWorkingCopy | undefined { - return this.files.get(resource) ?? this.untitled.get(resource); + get(resource: URI): IUntitledFileWorkingCopy | IStoredFileWorkingCopy | undefined { + return this.stored.get(resource) ?? this.untitled.get(resource); } //#endregion @@ -201,10 +203,10 @@ export class FileWorkingCopyManager2>; resolve(options?: INewUntitledFileWorkingCopyWithAssociatedResourceOptions): Promise>; resolve(options?: INewOrExistingUntitledFileWorkingCopyOptions): Promise>; - resolve(resource: URI, options?: IFileWorkingCopyResolveOptions): Promise>; - resolve(arg1?: URI | INewUntitledFileWorkingCopyOptions | INewUntitledFileWorkingCopyWithAssociatedResourceOptions | INewOrExistingUntitledFileWorkingCopyOptions, arg2?: IFileWorkingCopyResolveOptions): Promise | IFileWorkingCopy> { + resolve(resource: URI, options?: IStoredFileWorkingCopyResolveOptions): Promise>; + resolve(arg1?: URI | INewUntitledFileWorkingCopyOptions | INewUntitledFileWorkingCopyWithAssociatedResourceOptions | INewOrExistingUntitledFileWorkingCopyOptions, arg2?: IStoredFileWorkingCopyResolveOptions): Promise | IStoredFileWorkingCopy> { if (URI.isUri(arg1)) { - return this.files.resolve(arg1, arg2); + return this.stored.resolve(arg1, arg2); } return this.untitled.resolve(arg1); @@ -214,7 +216,7 @@ export class FileWorkingCopyManager2 | undefined> { + async saveAs(source: URI, target?: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { // Get to target resource if (!target) { @@ -256,22 +258,22 @@ export class FileWorkingCopyManager2 | undefined> { + private async doSave(resource: URI, options?: ISaveOptions): Promise | undefined> { - // Save is only possible with file working copies, + // Save is only possible with stored file working copies, // any other have to go via `saveAs` flow. - const fileWorkingCopy = this.files.get(resource); - if (fileWorkingCopy) { - const success = await fileWorkingCopy.save(options); + const storedFileWorkingCopy = this.stored.get(resource); + if (storedFileWorkingCopy) { + const success = await storedFileWorkingCopy.save(options); if (success) { - return fileWorkingCopy; + return storedFileWorkingCopy; } } return undefined; } - private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { + private async doSaveAs(source: URI, target: URI, options?: IFileWorkingCopySaveAsOptions): Promise | undefined> { let sourceContents: VSBufferReadableStream; // If the source is an existing file working copy, we can directly @@ -287,7 +289,7 @@ export class FileWorkingCopyManager2 }> { + private async doResolveSaveTarget(source: URI, target: URI): Promise<{ targetFileExists: boolean, targetStoredFileWorkingCopy: IStoredFileWorkingCopy }> { - // Prefer an existing file working copy if it is already resolved + // Prefer an existing stored file working copy if it is already resolved // for the given target resource let targetFileExists = false; - let targetFileWorkingCopy = this.files.get(target); - if (targetFileWorkingCopy?.isResolved()) { + let targetStoredFileWorkingCopy = this.stored.get(target); + if (targetStoredFileWorkingCopy?.isResolved()) { targetFileExists = true; } @@ -344,13 +346,13 @@ export class FileWorkingCopyManager2 { @@ -393,7 +395,7 @@ export class FileWorkingCopyManager2 { await Promises.settled([ - this.files.destroy(), + this.stored.destroy(), this.untitled.destroy() ]); } diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts similarity index 81% rename from src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts rename to src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index b81bc57845f..ee6091e2297 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -30,28 +30,28 @@ import { IResourceWorkingCopy, ResourceWorkingCopy } from 'vs/workbench/services import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel, IBaseFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; /** - * File specific working copy model factory. + * Stored file specific working copy model factory. */ -export interface IFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } +export interface IStoredFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } /** - * The underlying model of a file working copy provides some - * methods for the file working copy to function. The model is + * The underlying model of a stored file working copy provides some + * methods for the stored file working copy to function. The model is * typically only available after the working copy has been * resolved via it's `resolve()` method. */ -export interface IFileWorkingCopyModel extends IBaseFileWorkingCopyModel { +export interface IStoredFileWorkingCopyModel extends IBaseFileWorkingCopyModel { - readonly onDidChangeContent: Event; + readonly onDidChangeContent: Event; /** * A version ID of the model. If a `onDidChangeContent` is fired * from the model and the last known saved `versionId` matches - * with the `model.versionId`, the file working copy will discard - * any dirty state. + * with the `model.versionId`, the stored file working copy will + * discard any dirty state. * * A use case is the following: - * - a file working copy gets edited and thus dirty + * - a stored file working copy gets edited and thus dirty * - the user triggers undo to revert the changes * - at this point the `versionId` should match the one we had saved * @@ -70,7 +70,7 @@ export interface IFileWorkingCopyModel extends IBaseFileWorkingCopyModel { pushStackElement(): void; } -export interface IFileWorkingCopyModelContentChangedEvent { +export interface IStoredFileWorkingCopyModelContentChangedEvent { /** * Flag that indicates that this event was generated while undoing. @@ -84,37 +84,37 @@ export interface IFileWorkingCopyModelContentChangedEvent { } /** - * A file based `IWorkingCopy` is backed by a `URI` from a + * A stored file based `IWorkingCopy` is backed by a `URI` from a * known file system provider. Given this assumption, a lot * of functionality can be built on top, such as saving in * a secure way to prevent data loss. */ -export interface IFileWorkingCopy extends IResourceWorkingCopy, IBaseFileWorkingCopy { +export interface IStoredFileWorkingCopy extends IResourceWorkingCopy, IBaseFileWorkingCopy { /** - * An event for when a file working copy was resolved. + * An event for when a stored file working copy was resolved. */ readonly onDidResolve: Event; /** - * An event for when a file working copy was saved successfully. + * An event for when a stored file working copy was saved successfully. */ readonly onDidSave: Event; /** - * An event indicating that a file working copy save operation failed. + * An event indicating that a stored file working copy save operation failed. */ readonly onDidSaveError: Event; /** - * An event for when the readonly state of the file working copy changes. + * An event for when the readonly state of the stored file working copy changes. */ readonly onDidChangeReadonly: Event; /** - * Resolves a file working copy. + * Resolves a stored file working copy. */ - resolve(options?: IFileWorkingCopyResolveOptions): Promise; + resolve(options?: IStoredFileWorkingCopyResolveOptions): Promise; /** * Explicitly sets the working copy to be dirty. @@ -122,12 +122,12 @@ export interface IFileWorkingCopy extends IReso markDirty(): void; /** - * Whether the file working copy is in the provided `state` + * Whether the stored file working copy is in the provided `state` * or not. * * @param state the `FileWorkingCopyState` to check on. */ - hasState(state: FileWorkingCopyState): boolean; + hasState(state: StoredFileWorkingCopyState): boolean; /** * Allows to join a state change away from the provided `state`. @@ -135,119 +135,119 @@ export interface IFileWorkingCopy extends IReso * @param state currently only `FileWorkingCopyState.PENDING_SAVE` * can be awaited on to resolve. */ - joinState(state: FileWorkingCopyState.PENDING_SAVE): Promise; + joinState(state: StoredFileWorkingCopyState.PENDING_SAVE): Promise; /** * Whether we have a resolved model or not. */ - isResolved(): this is IResolvedFileWorkingCopy; + isResolved(): this is IResolvedStoredFileWorkingCopy; /** - * Whether the file working copy is readonly or not. + * Whether the stored file working copy is readonly or not. */ isReadonly(): boolean; } -export interface IResolvedFileWorkingCopy extends IFileWorkingCopy { +export interface IResolvedStoredFileWorkingCopy extends IStoredFileWorkingCopy { /** - * A resolved file working copy has a resolved model. + * A resolved stored file working copy has a resolved model. */ readonly model: M; } /** - * States the file working copy can be in. + * States the stored file working copy can be in. */ -export const enum FileWorkingCopyState { +export const enum StoredFileWorkingCopyState { /** - * A file working copy is saved. + * A stored file working copy is saved. */ SAVED, /** - * A file working copy is dirty. + * A stored file working copy is dirty. */ DIRTY, /** - * A file working copy is currently being saved but + * A stored file working copy is currently being saved but * this operation has not completed yet. */ PENDING_SAVE, /** - * A file working copy is in conflict mode when changes + * A stored file working copy is in conflict mode when changes * cannot be saved because the underlying file has changed. - * File working copies in conflict mode are always dirty. + * Stored file working copies in conflict mode are always dirty. */ CONFLICT, /** - * A file working copy is in orphan state when the underlying + * A stored file working copy is in orphan state when the underlying * file has been deleted. */ ORPHAN, /** * Any error that happens during a save that is not causing - * the `FileWorkingCopyState.CONFLICT` state. - * File working copies in error mode are always dirty. + * the `StoredFileWorkingCopyState.CONFLICT` state. + * Stored file working copies in error mode are always dirty. */ ERROR } -export interface IFileWorkingCopySaveOptions extends ISaveOptions { +export interface IStoredFileWorkingCopySaveOptions extends ISaveOptions { /** - * Save the file working copy with an attempt to unlock it. + * Save the stored file working copy with an attempt to unlock it. */ writeUnlock?: boolean; /** - * Save the file working copy with elevated privileges. + * Save the stored file working copy with elevated privileges. * * Note: This may not be supported in all environments. */ writeElevated?: boolean; /** - * Allows to write to a file working copy even if it has been + * Allows to write to a stored file working copy even if it has been * modified on disk. This should only be triggered from an * explicit user action. */ ignoreModifiedSince?: boolean; /** - * If set, will bubble up the file working copy save error to + * If set, will bubble up the stored file working copy save error to * the caller instead of handling it. */ ignoreErrorHandler?: boolean; } -export interface IFileWorkingCopyResolveOptions { +export interface IStoredFileWorkingCopyResolveOptions { /** - * The contents to use for the file working copy if known. If not + * The contents to use for the stored file working copy if known. If not * provided, the contents will be retrieved from the underlying * resource or backup if present. * - * If contents are provided, the file working copy will be marked + * If contents are provided, the stored file working copy will be marked * as dirty right from the beginning. */ contents?: VSBufferReadableStream; /** - * Go to disk bypassing any cache of the file working copy if any. + * Go to disk bypassing any cache of the stored file working copy if any. */ forceReadFromFile?: boolean; } /** - * Metadata associated with a file working copy backup. + * Metadata associated with a stored file working copy backup. */ -interface IFileWorkingCopyBackupMetaData extends IWorkingCopyBackupMeta { +interface IStoredFileWorkingCopyBackupMetaData extends IWorkingCopyBackupMeta { mtime: number; ctime: number; size: number; @@ -255,7 +255,7 @@ interface IFileWorkingCopyBackupMetaData extends IWorkingCopyBackupMeta { orphaned: boolean; } -export class FileWorkingCopy extends ResourceWorkingCopy implements IFileWorkingCopy { +export class StoredFileWorkingCopy extends ResourceWorkingCopy implements IStoredFileWorkingCopy { readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.None; @@ -291,7 +291,7 @@ export class FileWorkingCopy extends ResourceWo readonly typeId: string, resource: URI, readonly name: string, - private readonly modelFactory: IFileWorkingCopyModelFactory, + private readonly modelFactory: IStoredFileWorkingCopyModelFactory, @IFileService fileService: IFileService, @ILogService private readonly logService: ILogService, @ITextFileService private readonly textFileService: ITextFileService, @@ -318,7 +318,7 @@ export class FileWorkingCopy extends ResourceWo private dirty = false; private savedVersionId: unknown; - isDirty(): this is IResolvedFileWorkingCopy { + isDirty(): this is IResolvedStoredFileWorkingCopy { return this.dirty; } @@ -379,16 +379,16 @@ export class FileWorkingCopy extends ResourceWo private lastResolvedFileStat: IFileStatWithMetadata | undefined; - isResolved(): this is IResolvedFileWorkingCopy { + isResolved(): this is IResolvedStoredFileWorkingCopy { return !!this.model; } - async resolve(options?: IFileWorkingCopyResolveOptions): Promise { - this.trace('[file working copy] resolve() - enter'); + async resolve(options?: IStoredFileWorkingCopyResolveOptions): Promise { + this.trace('[stored file working copy] resolve() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[file working copy] resolve() - exit - without resolving because file working copy is disposed'); + this.trace('[stored file working copy] resolve() - exit - without resolving because file working copy is disposed'); return; } @@ -397,7 +397,7 @@ export class FileWorkingCopy extends ResourceWo // resolve a working copy that is dirty or is in the process of saving to prevent // data loss. if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { - this.trace('[file working copy] resolve() - exit - without resolving because file working copy is dirty or being saved'); + this.trace('[stored file working copy] resolve() - exit - without resolving because file working copy is dirty or being saved'); return; } @@ -405,7 +405,7 @@ export class FileWorkingCopy extends ResourceWo return this.doResolve(options); } - private async doResolve(options?: IFileWorkingCopyResolveOptions): Promise { + private async doResolve(options?: IStoredFileWorkingCopyResolveOptions): Promise { // First check if we have contents to use for the working copy if (options?.contents) { @@ -426,7 +426,7 @@ export class FileWorkingCopy extends ResourceWo } private async resolveFromBuffer(buffer: VSBufferReadableStream): Promise { - this.trace('[file working copy] resolveFromBuffer()'); + this.trace('[stored file working copy] resolveFromBuffer()'); // Try to resolve metdata from disk let mtime: number; @@ -470,12 +470,12 @@ export class FileWorkingCopy extends ResourceWo private async resolveFromBackup(): Promise { // Resolve backup if any - const backup = await this.workingCopyBackupService.resolve(this); + const backup = await this.workingCopyBackupService.resolve(this); // Abort if someone else managed to resolve the working copy by now let isNew = !this.isResolved(); if (!isNew) { - this.trace('[file working copy] resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); + this.trace('[stored file working copy] resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); return true; // imply that resolving has happened in another operation } @@ -491,8 +491,8 @@ export class FileWorkingCopy extends ResourceWo return false; } - private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup): Promise { - this.trace('[file working copy] doResolveFromBackup()'); + private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup): Promise { + this.trace('[stored file working copy] doResolveFromBackup()'); // Resolve with backup await this.resolveFromContent({ @@ -512,8 +512,8 @@ export class FileWorkingCopy extends ResourceWo } } - private async resolveFromFile(options?: IFileWorkingCopyResolveOptions): Promise { - this.trace('[file working copy] resolveFromFile()'); + private async resolveFromFile(options?: IStoredFileWorkingCopyResolveOptions): Promise { + this.trace('[stored file working copy] resolveFromFile()'); const forceReadFromFile = options?.forceReadFromFile; @@ -540,7 +540,7 @@ export class FileWorkingCopy extends ResourceWo // Return early if the working copy content has changed // meanwhile to prevent loosing any changes if (currentVersionId !== this.versionId) { - this.trace('[file working copy] resolveFromFile() - exit - without resolving because file working copy content changed'); + this.trace('[stored file working copy] resolveFromFile() - exit - without resolving because file working copy content changed'); return; } @@ -572,11 +572,11 @@ export class FileWorkingCopy extends ResourceWo } private async resolveFromContent(content: IFileStreamContent, dirty: boolean): Promise { - this.trace('[file working copy] resolveFromContent() - enter'); + this.trace('[stored file working copy] resolveFromContent() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[file working copy] resolveFromContent() - exit - because working copy is disposed'); + this.trace('[stored file working copy] resolveFromContent() - exit - because working copy is disposed'); return; } @@ -617,7 +617,7 @@ export class FileWorkingCopy extends ResourceWo } private async doCreateModel(contents: VSBufferReadableStream): Promise { - this.trace('[file working copy] doCreateModel()'); + this.trace('[stored file working copy] doCreateModel()'); // Create model and dispose it when we get disposed this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); @@ -629,7 +629,7 @@ export class FileWorkingCopy extends ResourceWo private ignoreDirtyOnModelContentChange = false; private async doUpdateModel(contents: VSBufferReadableStream): Promise { - this.trace('[file working copy] doUpdateModel()'); + this.trace('[stored file working copy] doUpdateModel()'); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -654,11 +654,11 @@ export class FileWorkingCopy extends ResourceWo } private onModelContentChanged(model: M, isUndoingOrRedoing: boolean): void { - this.trace(`[file working copy] onModelContentChanged() - enter`); + this.trace(`[stored file working copy] onModelContentChanged() - enter`); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; - this.trace(`[file working copy] onModelContentChanged() - new versionId ${this.versionId}`); + this.trace(`[stored file working copy] onModelContentChanged() - new versionId ${this.versionId}`); // Remember when the user changed the model through a undo/redo operation. // We need this information to throttle save participants to fix @@ -675,7 +675,7 @@ export class FileWorkingCopy extends ResourceWo // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. if (model.versionId === this.savedVersionId) { - this.trace('[file working copy] onModelContentChanged() - model content changed back to last saved version'); + this.trace('[stored file working copy] onModelContentChanged() - model content changed back to last saved version'); // Clear flags const wasDirty = this.dirty; @@ -689,7 +689,7 @@ export class FileWorkingCopy extends ResourceWo // Otherwise the content has changed and we signal this as becoming dirty else { - this.trace('[file working copy] onModelContentChanged() - model content changed and marked as dirty'); + this.trace('[stored file working copy] onModelContentChanged() - model content changed and marked as dirty'); // Mark as dirty this.setDirty(true); @@ -707,7 +707,7 @@ export class FileWorkingCopy extends ResourceWo async backup(token: CancellationToken): Promise { // Fill in metadata if we are resolved - let meta: IFileWorkingCopyBackupMetaData | undefined = undefined; + let meta: IStoredFileWorkingCopyBackupMetaData | undefined = undefined; if (this.lastResolvedFileStat) { meta = { mtime: this.lastResolvedFileStat.mtime, @@ -738,41 +738,41 @@ export class FileWorkingCopy extends ResourceWo private readonly saveSequentializer = new TaskSequentializer(); - async save(options: IFileWorkingCopySaveOptions = Object.create(null)): Promise { + async save(options: IStoredFileWorkingCopySaveOptions = Object.create(null)): Promise { if (!this.isResolved()) { return false; } if (this.isReadonly()) { - this.trace('[file working copy] save() - ignoring request for readonly resource'); + this.trace('[stored file working copy] save() - ignoring request for readonly resource'); return false; // if working copy is readonly we do not attempt to save at all } if ( - (this.hasState(FileWorkingCopyState.CONFLICT) || this.hasState(FileWorkingCopyState.ERROR)) && + (this.hasState(StoredFileWorkingCopyState.CONFLICT) || this.hasState(StoredFileWorkingCopyState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) ) { - this.trace('[file working copy] save() - ignoring auto save request for file working copy that is in conflict or error'); + this.trace('[stored file working copy] save() - ignoring auto save request for file working copy that is in conflict or error'); return false; // if working copy is in save conflict or error, do not save unless save reason is explicit } // Actually do save - this.trace('[file working copy] save() - enter'); + this.trace('[stored file working copy] save() - enter'); await this.doSave(options); - this.trace('[file working copy] save() - exit'); + this.trace('[stored file working copy] save() - exit'); return true; } - private async doSave(options: IFileWorkingCopySaveOptions): Promise { + private async doSave(options: IStoredFileWorkingCopySaveOptions): Promise { if (typeof options.reason !== 'number') { options.reason = SaveReason.EXPLICIT; } let versionId = this.versionId; - this.trace(`[file working copy] doSave(${versionId}) - enter with versionId ${versionId}`); + this.trace(`[stored file working copy] doSave(${versionId}) - enter with versionId ${versionId}`); // Lookup any running pending save for this versionId and return it if found // @@ -780,7 +780,7 @@ export class FileWorkingCopy extends ResourceWo // while the save was not yet finished to disk // if (this.saveSequentializer.hasPending(versionId)) { - this.trace(`[file working copy] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); + this.trace(`[stored file working copy] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); return this.saveSequentializer.pending; } @@ -789,7 +789,7 @@ export class FileWorkingCopy extends ResourceWo // // Scenario: user invoked save action even though the working copy is not dirty if (!options.force && !this.dirty) { - this.trace(`[file working copy] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); + this.trace(`[stored file working copy] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); return; } @@ -803,7 +803,7 @@ export class FileWorkingCopy extends ResourceWo // while the first save has not returned yet. // if (this.saveSequentializer.hasPending()) { - this.trace(`[file working copy] doSave(${versionId}) - exit - because busy saving`); + this.trace(`[stored file working copy] doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to // cancel the pending operation so that ours can run @@ -852,8 +852,8 @@ export class FileWorkingCopy extends ResourceWo // Reported as: https://github.com/microsoft/vscode/issues/102542 if (options.reason === SaveReason.AUTO && typeof this.lastContentChangeFromUndoRedo === 'number') { const timeFromUndoRedoToSave = Date.now() - this.lastContentChangeFromUndoRedo; - if (timeFromUndoRedoToSave < FileWorkingCopy.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD) { - await timeout(FileWorkingCopy.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD - timeFromUndoRedoToSave); + if (timeFromUndoRedoToSave < StoredFileWorkingCopy.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD) { + await timeout(StoredFileWorkingCopy.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD - timeFromUndoRedoToSave); } } @@ -862,7 +862,7 @@ export class FileWorkingCopy extends ResourceWo await this.textFileService.files.runSaveParticipants(this.model, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); } } catch (error) { - this.logService.error(`[file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true), this.typeId); + this.logService.error(`[stored file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true), this.typeId); } } @@ -896,7 +896,7 @@ export class FileWorkingCopy extends ResourceWo // Save to Disk. We mark the save operation as currently pending with // the latest versionId because it might have changed from a save // participant triggering - this.trace(`[file working copy] doSave(${versionId}) - before write()`); + this.trace(`[stored file working copy] doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedFileWorkingCopy = this; return this.saveSequentializer.setPending(versionId, (async () => { @@ -939,17 +939,17 @@ export class FileWorkingCopy extends ResourceWo })(), () => saveCancellation.cancel()); } - private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: IFileWorkingCopySaveOptions): void { + private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: IStoredFileWorkingCopySaveOptions): void { // Updated resolved stat with updated stat this.updateLastResolvedFileStat(stat); // Update dirty state unless working copy has changed meanwhile if (versionId === this.versionId) { - this.trace(`[file working copy] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); + this.trace(`[stored file working copy] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); this.setDirty(false); } else { - this.trace(`[file working copy] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); + this.trace(`[stored file working copy] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); } // Update orphan state given save was successful @@ -959,8 +959,8 @@ export class FileWorkingCopy extends ResourceWo this._onDidSave.fire(options.reason ?? SaveReason.EXPLICIT); } - private handleSaveError(error: Error, versionId: number, options: IFileWorkingCopySaveOptions): void { - this.logService.error(`[file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true), this.typeId); + private handleSaveError(error: Error, versionId: number, options: IStoredFileWorkingCopySaveOptions): void { + this.logService.error(`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true), this.typeId); // Return early if the save() call was made asking to // handle the save error itself. @@ -1110,7 +1110,7 @@ export class FileWorkingCopy extends ResourceWo return; // ignore if not resolved or not dirty and not enforced } - this.trace('[file working copy] revert()'); + this.trace('[stored file working copy] revert()'); // Unset flags const wasDirty = this.dirty; @@ -1150,24 +1150,24 @@ export class FileWorkingCopy extends ResourceWo private inConflictMode = false; private inErrorMode = false; - hasState(state: FileWorkingCopyState): boolean { + hasState(state: StoredFileWorkingCopyState): boolean { switch (state) { - case FileWorkingCopyState.CONFLICT: + case StoredFileWorkingCopyState.CONFLICT: return this.inConflictMode; - case FileWorkingCopyState.DIRTY: + case StoredFileWorkingCopyState.DIRTY: return this.dirty; - case FileWorkingCopyState.ERROR: + case StoredFileWorkingCopyState.ERROR: return this.inErrorMode; - case FileWorkingCopyState.ORPHAN: + case StoredFileWorkingCopyState.ORPHAN: return this.isOrphaned(); - case FileWorkingCopyState.PENDING_SAVE: + case StoredFileWorkingCopyState.PENDING_SAVE: return this.saveSequentializer.hasPending(); - case FileWorkingCopyState.SAVED: + case StoredFileWorkingCopyState.SAVED: return !this.dirty; } } - joinState(state: FileWorkingCopyState.PENDING_SAVE): Promise { + joinState(state: StoredFileWorkingCopyState.PENDING_SAVE): Promise { return this.saveSequentializer.pending ?? Promise.resolve(); } @@ -1188,7 +1188,7 @@ export class FileWorkingCopy extends ResourceWo //#region Dispose override dispose(): void { - this.trace('[file working copy] dispose()'); + this.trace('[stored file working copy] dispose()'); // State this.inConflictMode = false; diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts similarity index 81% rename from src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts rename to src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index 5a45b6b8279..567bb245767 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -5,7 +5,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory, IFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { SaveReason } from 'vs/workbench/common/editor'; import { ResourceMap } from 'vs/base/common/map'; import { Promises, ResourceQueue } from 'vs/base/common/async'; @@ -31,116 +31,117 @@ import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/com import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; /** - * The only one that should be dealing with `IFileWorkingCopy` and handle all + * The only one that should be dealing with `IStoredFileWorkingCopy` and handle all * operations that are working copy related, such as save/revert, backup * and resolving. */ -export interface IFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { +export interface IStoredFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { /** - * An event for when a file working copy was resolved. + * An event for when a stored file working copy was resolved. */ - readonly onDidResolve: Event>; + readonly onDidResolve: Event>; /** - * An event for when a file working copy changed it's dirty state. + * An event for when a stored file working copy changed it's dirty state. */ - readonly onDidChangeDirty: Event>; + readonly onDidChangeDirty: Event>; /** - * An event for when a file working copy failed to save. + * An event for when a stored file working copy failed to save. */ - readonly onDidSaveError: Event>; + readonly onDidSaveError: Event>; /** - * An event for when a file working copy successfully saved. + * An event for when a stored file working copy successfully saved. */ - readonly onDidSave: Event>; + readonly onDidSave: Event>; /** - * An event for when a file working copy was reverted. + * An event for when a stored file working copy was reverted. */ - readonly onDidRevert: Event>; + readonly onDidRevert: Event>; /** - * Allows to resolve a file working copy. If the manager already knows - * about a file working copy with the same `URI`, it will return that - * existing file working copy. There will never be more than one - * file working copy per `URI` until the file working copy is disposed. + * Allows to resolve a stored file working copy. If the manager already knows + * about a stored file working copy with the same `URI`, it will return that + * existing stored file working copy. There will never be more than one + * stored file working copy per `URI` until the stored file working copy is + * disposed. * - * Use the `IFileWorkingCopyResolveOptions.reload` option to control the - * behaviour for when a file working copy was previously already resolved + * Use the `IStoredFileWorkingCopyResolveOptions.reload` option to control the + * behaviour for when a stored file working copy was previously already resolved * with regards to resolving it again from the underlying file resource * or not. * * Note: Callers must `dispose` the working copy when no longer needed. * - * @param resource used as unique identifier of the file working copy in + * @param resource used as unique identifier of the stored file working copy in * case one is already known for this `URI`. * @param options */ - resolve(resource: URI, options?: IFileWorkingCopyManagerResolveOptions): Promise>; + resolve(resource: URI, options?: IStoredFileWorkingCopyManagerResolveOptions): Promise>; /** - * Waits for the file working copy to be ready to be disposed. There may be - * conditions under which the file working copy cannot be disposed, e.g. when + * Waits for the stored file working copy to be ready to be disposed. There may be + * conditions under which the stored file working copy cannot be disposed, e.g. when * it is dirty. Once the promise is settled, it is safe to dispose. */ - canDispose(workingCopy: IFileWorkingCopy): true | Promise; + canDispose(workingCopy: IStoredFileWorkingCopy): true | Promise; /** * @deprecated */ - saveAs(source: URI, target: URI): Promise | undefined>; - saveAs(source: URI, target: undefined): Promise | undefined>; + saveAs(source: URI, target: URI): Promise | undefined>; + saveAs(source: URI, target: undefined): Promise | undefined>; } -export interface IFileWorkingCopySaveEvent { +export interface IStoredFileWorkingCopySaveEvent { /** - * The file working copy that was successfully saved. + * The stored file working copy that was successfully saved. */ - workingCopy: IFileWorkingCopy; + workingCopy: IStoredFileWorkingCopy; /** - * The reason why the file working copy was saved. + * The reason why the stored file working copy was saved. */ reason: SaveReason; } -export interface IFileWorkingCopyManagerResolveOptions extends IFileWorkingCopyResolveOptions { +export interface IStoredFileWorkingCopyManagerResolveOptions extends IStoredFileWorkingCopyResolveOptions { /** - * If the file working copy was already resolved before, + * If the stored file working copy was already resolved before, * allows to trigger a reload of it to fetch the latest contents: * - async: resolve() will return immediately and trigger * a reload that will run in the background. * - sync: resolve() will only return resolved when the - * file working copy has finished reloading. + * stored file working copy has finished reloading. */ reload?: { async: boolean }; } -export class FileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IFileWorkingCopyManager { +export class StoredFileWorkingCopyManager extends BaseFileWorkingCopyManager> implements IStoredFileWorkingCopyManager { //#region Events - private readonly _onDidResolve = this._register(new Emitter>()); + private readonly _onDidResolve = this._register(new Emitter>()); readonly onDidResolve = this._onDidResolve.event; - private readonly _onDidChangeDirty = this._register(new Emitter>()); + private readonly _onDidChangeDirty = this._register(new Emitter>()); readonly onDidChangeDirty = this._onDidChangeDirty.event; - private readonly _onDidSaveError = this._register(new Emitter>()); + private readonly _onDidSaveError = this._register(new Emitter>()); readonly onDidSaveError = this._onDidSaveError.event; - private readonly _onDidSave = this._register(new Emitter>()); + private readonly _onDidSave = this._register(new Emitter>()); readonly onDidSave = this._onDidSave.event; - private readonly _onDidRevert = this._register(new Emitter>()); + private readonly _onDidRevert = this._register(new Emitter>()); readonly onDidRevert = this._onDidRevert.event; //#endregion @@ -152,7 +153,7 @@ export class FileWorkingCopyManager extends Bas constructor( private readonly workingCopyTypeId: string, - private readonly modelFactory: IFileWorkingCopyModelFactory, + private readonly modelFactory: IStoredFileWorkingCopyModelFactory, @IFileService fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @ILabelService private readonly labelService: ILabelService, @@ -189,13 +190,13 @@ export class FileWorkingCopyManager extends Bas } private async onWillShutdown(): Promise { - let fileWorkingCopies: IFileWorkingCopy[]; + let fileWorkingCopies: IStoredFileWorkingCopy[]; - // As long as file working copies are pending to be saved, we prolong the shutdown + // As long as stored file working copies are pending to be saved, we prolong the shutdown // until that has happened to ensure we are not shutting down in the middle of // writing to the working copy (https://github.com/microsoft/vscode/issues/116600). - while ((fileWorkingCopies = this.workingCopies.filter(workingCopy => workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE))).length > 0) { - await Promises.settled(fileWorkingCopies.map(workingCopy => workingCopy.joinState(FileWorkingCopyState.PENDING_SAVE))); + while ((fileWorkingCopies = this.workingCopies.filter(workingCopy => workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE))).length > 0) { + await Promises.settled(fileWorkingCopies.map(workingCopy => workingCopy.joinState(StoredFileWorkingCopyState.PENDING_SAVE))); } } @@ -217,7 +218,7 @@ export class FileWorkingCopyManager extends Bas } } - private queueWorkingCopyResolve(workingCopy: IFileWorkingCopy): void { + private queueWorkingCopyResolve(workingCopy: IStoredFileWorkingCopy): void { // Resolves a working copy to update (use a queue to prevent accumulation of // resolve when the resolving actually takes long. At most we only want the @@ -254,7 +255,7 @@ export class FileWorkingCopyManager extends Bas } // Find all working copies that related to source (can be many if resource is a folder) - const sourceWorkingCopies: IFileWorkingCopy[] = []; + const sourceWorkingCopies: IStoredFileWorkingCopy[] = []; for (const workingCopy of this.workingCopies) { if (this.uriIdentityService.extUri.isEqualOrParent(workingCopy.resource, source)) { sourceWorkingCopies.push(workingCopy); @@ -358,7 +359,7 @@ export class FileWorkingCopyManager extends Bas //#region Resolve - async resolve(resource: URI, options?: IFileWorkingCopyManagerResolveOptions): Promise> { + async resolve(resource: URI, options?: IStoredFileWorkingCopyManagerResolveOptions): Promise> { // Await a pending working copy resolve first before proceeding // to ensure that we never resolve a working copy more than once @@ -401,11 +402,11 @@ export class FileWorkingCopyManager extends Bas } } - // File working copy does not exist + // Stored file working copy does not exist else { didCreateWorkingCopy = true; - workingCopy = new FileWorkingCopy( + workingCopy = new StoredFileWorkingCopy( this.workingCopyTypeId, resource, this.labelService.getUriBasenameLabel(resource), @@ -444,7 +445,7 @@ export class FileWorkingCopyManager extends Bas // Remove from pending resolves this.mapResourceToPendingWorkingCopyResolve.delete(resource); - // File working copy can be dirty if a backup was restored, so we make sure to + // Stored file working copy can be dirty if a backup was restored, so we make sure to // have this event delivered if we created the working copy here if (didCreateWorkingCopy && workingCopy.isDirty()) { this._onDidChangeDirty.fire(workingCopy); @@ -474,7 +475,7 @@ export class FileWorkingCopyManager extends Bas return undefined; } - private registerWorkingCopy(workingCopy: IFileWorkingCopy): void { + private registerWorkingCopy(workingCopy: IStoredFileWorkingCopy): void { // Install working copy listeners const workingCopyListeners = new DisposableStore(); @@ -503,7 +504,7 @@ export class FileWorkingCopyManager extends Bas //#region Lifecycle - canDispose(workingCopy: IFileWorkingCopy): true | Promise { + canDispose(workingCopy: IStoredFileWorkingCopy): true | Promise { // Quick return if working copy already disposed or not dirty and not resolving if ( @@ -517,7 +518,7 @@ export class FileWorkingCopyManager extends Bas return this.doCanDispose(workingCopy); } - private async doCanDispose(workingCopy: IFileWorkingCopy): Promise { + private async doCanDispose(workingCopy: IStoredFileWorkingCopy): Promise { // If we have a pending working copy resolve, await it first and then try again const pendingResolve = this.joinPendingResolve(workingCopy.resource); @@ -558,7 +559,7 @@ export class FileWorkingCopyManager extends Bas /** * @deprecated TODO@bpasero remove */ - async saveAs(source: URI, target?: URI): Promise | undefined> { + async saveAs(source: URI, target?: URI): Promise | undefined> { // If not provided, ask user for target if (!target) { @@ -573,10 +574,10 @@ export class FileWorkingCopyManager extends Bas return this.doSaveAs(source, target); } - private async doSaveAs(source: URI, target: URI): Promise | undefined> { + private async doSaveAs(source: URI, target: URI): Promise | undefined> { let sourceContents: VSBufferReadableStream; - // If the source is an existing file working copy, we can directly + // If the source is an existing stored file working copy, we can directly // use that to copy the contents to the target destination const sourceWorkingCopy = this.get(source); if (sourceWorkingCopy?.isResolved()) { @@ -593,7 +594,7 @@ export class FileWorkingCopyManager extends Bas return this.doSaveAsWorkingCopy(source, sourceContents, target); } - private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI): Promise> { + private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI): Promise> { // Prefer an existing working copy if it is already resolved // for the given target resource diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts index 3c25fc4ae3e..c89ef474196 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts @@ -7,9 +7,9 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, TestInMemoryFileSystemProvider } from 'vs/workbench/test/browser/workbenchTestServices'; -import { FileWorkingCopy, IFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { StoredFileWorkingCopy, IStoredFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; -import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; +import { TestStoredFileWorkingCopyModel, TestStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test'; import { Schemas } from 'vs/base/common/network'; import { IFileWorkingCopyManager2, FileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; @@ -20,7 +20,7 @@ suite('FileWorkingCopyManager2', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let manager: IFileWorkingCopyManager2; + let manager: IFileWorkingCopyManager2; setup(() => { instantiationService = workbenchInstantiationService(); @@ -31,7 +31,7 @@ suite('FileWorkingCopyManager2', () => { manager = new FileWorkingCopyManager2( 'testFileWorkingCopyType2', - new TestFileWorkingCopyModelFactory(), + new TestStoredFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, @@ -77,8 +77,8 @@ suite('FileWorkingCopyManager2', () => { test('resolve', async () => { const fileWorkingCopy = await manager.resolve(URI.file('/test.html')); - assert.ok(fileWorkingCopy instanceof FileWorkingCopy); - assert.strictEqual(await manager.files.resolve(fileWorkingCopy.resource), fileWorkingCopy); + assert.ok(fileWorkingCopy instanceof StoredFileWorkingCopy); + assert.strictEqual(await manager.stored.resolve(fileWorkingCopy.resource), fileWorkingCopy); const untitledFileWorkingCopy = await manager.resolve(); assert.ok(untitledFileWorkingCopy instanceof UntitledFileWorkingCopy); @@ -95,13 +95,13 @@ suite('FileWorkingCopyManager2', () => { await manager.resolve({ contents: bufferToStream(VSBuffer.fromString('Hello Untitled')) }); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 2); - assert.strictEqual(manager.files.workingCopies.length, 1); + assert.strictEqual(manager.stored.workingCopies.length, 1); assert.strictEqual(manager.untitled.workingCopies.length, 1); await manager.destroy(); assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); - assert.strictEqual(manager.files.workingCopies.length, 0); + assert.strictEqual(manager.stored.workingCopies.length, 0); assert.strictEqual(manager.untitled.workingCopies.length, 0); }); @@ -179,14 +179,14 @@ suite('FileWorkingCopyManager2', () => { }); async function testSaveAsFile(source: URI, target: URI, resolveSource: boolean, resolveTarget: boolean) { - let sourceWorkingCopy: IFileWorkingCopy | undefined = undefined; + let sourceWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; if (resolveSource) { sourceWorkingCopy = await manager.resolve(source); sourceWorkingCopy.model?.updateContents('hello world'); assert.ok(sourceWorkingCopy.isDirty()); } - let targetWorkingCopy: IFileWorkingCopy | undefined = undefined; + let targetWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; if (resolveTarget) { targetWorkingCopy = await manager.resolve(target); targetWorkingCopy.model?.updateContents('hello world'); @@ -231,7 +231,7 @@ suite('FileWorkingCopyManager2', () => { const result = await manager.saveAs(workingCopy.resource, undefined); assert.strictEqual(result?.resource.toString(), target.toString()); - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + assert.strictEqual((result?.model as TestStoredFileWorkingCopyModel).contents, 'Simple Save As'); assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); @@ -249,7 +249,7 @@ suite('FileWorkingCopyManager2', () => { const result = await manager.saveAs(workingCopy.resource, undefined); assert.strictEqual(result?.resource.toString(), target.toString()); - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); + assert.strictEqual((result?.model as TestStoredFileWorkingCopyModel).contents, 'Simple Save As with associated resource'); assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); @@ -267,7 +267,7 @@ suite('FileWorkingCopyManager2', () => { const result = await manager.saveAs(workingCopy.resource, undefined); assert.strictEqual(result, targetFileWorkingCopy); - assert.strictEqual((result?.model as TestFileWorkingCopyModel).contents, 'Simple Save As'); + assert.strictEqual((result?.model as TestStoredFileWorkingCopyModel).contents, 'Simple Save As'); assert.strictEqual(manager.untitled.get(workingCopy.resource), undefined); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts similarity index 78% rename from src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts rename to src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index cfbfbdfc664..37a28f22ecf 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { FileWorkingCopy, FileWorkingCopyState, IFileWorkingCopyModel, IFileWorkingCopyModelContentChangedEvent, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { bufferToStream, newWriteableBufferStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -18,9 +18,9 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { Promises } from 'vs/base/common/async'; import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; -export class TestFileWorkingCopyModel extends Disposable implements IFileWorkingCopyModel { +export class TestStoredFileWorkingCopyModel extends Disposable implements IStoredFileWorkingCopyModel { - private readonly _onDidChangeContent = this._register(new Emitter()); + private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; private readonly _onWillDispose = this._register(new Emitter()); @@ -30,7 +30,7 @@ export class TestFileWorkingCopyModel extends Disposable implements IFileWorking super(); } - fireContentChangeEvent(event: IFileWorkingCopyModelContentChangedEvent): void { + fireContentChangeEvent(event: IStoredFileWorkingCopyModelContentChangedEvent): void { this._onDidChangeContent.fire(event); } @@ -81,24 +81,24 @@ export class TestFileWorkingCopyModel extends Disposable implements IFileWorking } } -export class TestFileWorkingCopyModelFactory implements IFileWorkingCopyModelFactory { +export class TestStoredFileWorkingCopyModelFactory implements IStoredFileWorkingCopyModelFactory { - async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise { - return new TestFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString()); + async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise { + return new TestStoredFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString()); } } -suite('FileWorkingCopy', function () { +suite('StoredFileWorkingCopy', function () { - const factory = new TestFileWorkingCopyModelFactory(); + const factory = new TestStoredFileWorkingCopyModelFactory(); let resource = URI.file('test/resource'); let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let workingCopy: FileWorkingCopy; + let workingCopy: StoredFileWorkingCopy; function createWorkingCopy(uri: URI = resource) { - return new FileWorkingCopy('testFileWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); + return new StoredFileWorkingCopy('testStoredFileWorkingCopyType', uri, basename(uri), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService); } setup(() => { @@ -125,26 +125,26 @@ suite('FileWorkingCopy', function () { }); test('orphaned tracking', async () => { - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); let onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); accessor.fileService.notExistsSet.set(resource, true); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await onDidChangeOrphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); onDidChangeOrphanedPromise = Event.toPromise(workingCopy.onDidChangeOrphaned); accessor.fileService.notExistsSet.delete(resource); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.ADDED }], false)); await onDidChangeOrphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); }); test('dirty', async () => { assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); await workingCopy.resolve(); assert.strictEqual(workingCopy.isResolved(), true); @@ -164,13 +164,13 @@ suite('FileWorkingCopy', function () { assert.strictEqual(contentChangeCounter, 1); assert.strictEqual(workingCopy.isDirty(), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); assert.strictEqual(changeDirtyCounter, 1); await workingCopy.save(); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); assert.strictEqual(changeDirtyCounter, 2); // Dirty from: Initial contents @@ -178,26 +178,26 @@ suite('FileWorkingCopy', function () { assert.strictEqual(contentChangeCounter, 2); // content of model did not change assert.strictEqual(workingCopy.isDirty(), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); assert.strictEqual(changeDirtyCounter, 3); await workingCopy.revert({ soft: true }); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); assert.strictEqual(changeDirtyCounter, 4); // Dirty from: API workingCopy.markDirty(); assert.strictEqual(workingCopy.isDirty(), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); assert.strictEqual(changeDirtyCounter, 5); await workingCopy.revert(); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); assert.strictEqual(changeDirtyCounter, 6); }); @@ -295,7 +295,7 @@ suite('FileWorkingCopy', function () { accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); const backup = await workingCopy.backup(CancellationToken.None); await accessor.workingCopyBackupService.backup(workingCopy, backup.content, undefined, backup.meta); @@ -307,7 +307,7 @@ suite('FileWorkingCopy', function () { workingCopy = createWorkingCopy(); await workingCopy.resolve(); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); const backup2 = await workingCopy.backup(CancellationToken.None); assert.deepStrictEqual(backup.meta, backup2.meta); @@ -322,18 +322,18 @@ suite('FileWorkingCopy', function () { accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); // resolving clears orphaned state when successful accessor.fileService.notExistsSet.delete(resource); await workingCopy.resolve({ forceReadFromFile: true }); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); // resolving adds orphaned state when fail to read try { accessor.fileService.readShouldThrowError = new FileOperationError('file not found', FileOperationResult.FILE_NOT_FOUND); await workingCopy.resolve(); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); } finally { accessor.fileService.readShouldThrowError = undefined; } @@ -468,13 +468,13 @@ suite('FileWorkingCopy', function () { accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.DELETED }], false)); await orphanedPromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), true); await workingCopy.save({ force: true }); assert.strictEqual(savedCounter, 6); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN), false); }); test('save (errors)', async () => { @@ -501,30 +501,30 @@ suite('FileWorkingCopy', function () { assert.strictEqual(savedCounter, 0); assert.strictEqual(saveErrorCounter, 1); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), false); assert.strictEqual(workingCopy.isDirty(), true); // save is a no-op unless forced when in error case await workingCopy.save({ reason: SaveReason.AUTO }); assert.strictEqual(savedCounter, 0); assert.strictEqual(saveErrorCounter, 1); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), false); assert.strictEqual(workingCopy.isDirty(), true); // save clears error flags when successful await workingCopy.save({ reason: SaveReason.EXPLICIT }); assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 1); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), false); assert.strictEqual(workingCopy.isDirty(), false); // save error: conflict @@ -540,20 +540,20 @@ suite('FileWorkingCopy', function () { assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 2); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), true); assert.strictEqual(workingCopy.isDirty(), true); // save clears error flags when successful await workingCopy.save({ reason: SaveReason.EXPLICIT }); assert.strictEqual(savedCounter, 2); assert.strictEqual(saveErrorCounter, 2); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ERROR), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.CONFLICT), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.CONFLICT), false); assert.strictEqual(workingCopy.isDirty(), false); }); @@ -632,34 +632,34 @@ suite('FileWorkingCopy', function () { }); test('state', async () => { - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); await workingCopy.resolve({ contents: bufferToStream(VSBuffer.fromString('hello state')) }); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); const savePromise = workingCopy.save(); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), true); await savePromise; - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); }); test('joinState', async () => { await workingCopy.resolve({ contents: bufferToStream(VSBuffer.fromString('hello state')) }); workingCopy.save(); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), true); - await workingCopy.joinState(FileWorkingCopyState.PENDING_SAVE); + await workingCopy.joinState(StoredFileWorkingCopyState.PENDING_SAVE); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.DIRTY), false); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.SAVED), true); - assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.PENDING_SAVE), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.SAVED), true); + assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.PENDING_SAVE), false); }); test('isReadonly, isResolved, dispose, isDisposed', async () => { diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts similarity index 94% rename from src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts rename to src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts index 7512a46504e..26fe7bdafca 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -7,28 +7,28 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; -import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; -import { IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; +import { StoredFileWorkingCopyManager, IStoredFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager'; +import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { timeout } from 'vs/base/common/async'; -import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; +import { TestStoredFileWorkingCopyModel, TestStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test'; import { CancellationToken } from 'vs/base/common/cancellation'; -suite('FileWorkingCopyManager', () => { +suite('StoredFileWorkingCopyManager', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let manager: IFileWorkingCopyManager; + let manager: IStoredFileWorkingCopyManager; setup(() => { instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); - manager = new FileWorkingCopyManager( - 'testFileWorkingCopyType', - new TestFileWorkingCopyModelFactory(), + manager = new StoredFileWorkingCopyManager( + 'testStoredFileWorkingCopyType', + new TestStoredFileWorkingCopyModelFactory(), accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, @@ -43,7 +43,7 @@ suite('FileWorkingCopyManager', () => { test('resolve', async () => { const resource = URI.file('/test.html'); - const events: IFileWorkingCopy[] = []; + const events: IStoredFileWorkingCopy[] = []; const listener = manager.onDidCreate(workingCopy => { events.push(workingCopy); }); @@ -55,7 +55,8 @@ suite('FileWorkingCopyManager', () => { const workingCopy1 = await resolvePromise; assert.ok(workingCopy1); assert.ok(workingCopy1.model); - assert.strictEqual(workingCopy1.typeId, 'testFileWorkingCopyType'); + assert.strictEqual(workingCopy1.typeId, 'testStoredFileWorkingCopyType'); + assert.strictEqual(workingCopy1.resource.toString(), resource.toString()); assert.strictEqual(manager.get(resource), workingCopy1); const workingCopy2 = await manager.resolve(resource); @@ -528,14 +529,14 @@ suite('FileWorkingCopyManager', () => { }); async function testSaveAs(source: URI, target: URI, resolveSource: boolean, resolveTarget: boolean) { - let sourceWorkingCopy: IFileWorkingCopy | undefined = undefined; + let sourceWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; if (resolveSource) { sourceWorkingCopy = await manager.resolve(source); sourceWorkingCopy.model?.updateContents('hello world'); assert.ok(sourceWorkingCopy.isDirty()); } - let targetWorkingCopy: IFileWorkingCopy | undefined = undefined; + let targetWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; if (resolveTarget) { targetWorkingCopy = await manager.resolve(target); targetWorkingCopy.model?.updateContents('hello world'); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 7032a0e2074..9cf2139f310 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileWorkingCopyManager2, IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; +import { TestStoredFileWorkingCopyModel, TestStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test'; import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; import { TestInMemoryFileSystemProvider, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; @@ -19,7 +19,7 @@ suite('UntitledFileWorkingCopyManager', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let manager: IFileWorkingCopyManager2; + let manager: IFileWorkingCopyManager2; setup(() => { instantiationService = workbenchInstantiationService(); @@ -29,8 +29,8 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); manager = new FileWorkingCopyManager2( - 'testFileWorkingCopyTypeUntitled', - new TestFileWorkingCopyModelFactory(), + 'testUntitledFileWorkingCopyType', + new TestStoredFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, @@ -69,6 +69,9 @@ suite('UntitledFileWorkingCopyManager', () => { const workingCopy1 = await manager.untitled.resolve(); const workingCopy2 = await manager.untitled.resolve(); + assert.strictEqual(workingCopy1.typeId, 'testUntitledFileWorkingCopyType'); + assert.strictEqual(workingCopy1.resource.scheme, Schemas.untitled); + assert.strictEqual(createCounter, 2); assert.strictEqual(manager.untitled.get(workingCopy1.resource), workingCopy1); From b8fe2db4392446a211767a00e8213d040786e54d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 08:37:29 +0200 Subject: [PATCH 238/270] fileworkingcopymanager2 => fileworkingcopymanager --- .../notebook/common/notebookEditorModel.ts | 4 +- .../notebookEditorModelResolverServiceImpl.ts | 8 +- .../common/abstractFileWorkingCopyManager.ts | 6 +- ...tFileWorkingCopy.ts => fileWorkingCopy.ts} | 19 +-- ...yManager2.ts => fileWorkingCopyManager.ts} | 12 +- .../common/storedFileWorkingCopy.ts | 8 +- .../common/storedFileWorkingCopyManager.ts | 106 ---------------- .../common/untitledFileWorkingCopy.ts | 8 +- ...test.ts => fileWorkingCopyManager.test.ts} | 10 +- .../storedFileWorkingCopyManager.test.ts | 118 +----------------- .../untitledFileWorkingCopyManager.test.ts | 6 +- 11 files changed, 42 insertions(+), 263 deletions(-) rename src/vs/workbench/services/workingCopy/common/{abstractFileWorkingCopy.ts => fileWorkingCopy.ts} (81%) rename src/vs/workbench/services/workingCopy/common/{fileWorkingCopyManager2.ts => fileWorkingCopyManager.ts} (96%) rename src/vs/workbench/services/workingCopy/test/browser/{fileWorkingCopyManager2.test.ts => fileWorkingCopyManager.test.ts} (96%) diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 5cba38973fb..ccc0da191b1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -29,7 +29,7 @@ import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { filter } from 'vs/base/common/objects'; -import { IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; +import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; //#region --- complex content provider @@ -432,7 +432,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE constructor( readonly resource: URI, readonly viewType: string, - private readonly _workingCopyManager: IFileWorkingCopyManager2, + private readonly _workingCopyManager: IFileWorkingCopyManager, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IFileService private readonly _fileService: IFileService ) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 75f9ce0c647..17babc5a8f1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -15,12 +15,12 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceMap } from 'vs/base/common/map'; -import { FileWorkingCopyManager2, IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; +import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; class NotebookModelReferenceCollection extends ReferenceCollection> { private readonly _disposables = new DisposableStore(); - private readonly _workingCopyManagers = new Map>(); + private readonly _workingCopyManagers = new Map>(); private readonly _modelListener = new Map(); private readonly _onDidSaveNotebook = new Emitter(); @@ -71,8 +71,8 @@ class NotebookModelReferenceCollection extends ReferenceCollection>this._instantiationService.createInstance( - FileWorkingCopyManager2, + workingCopyManager = >this._instantiationService.createInstance( + FileWorkingCopyManager, workingCopyTypeId, factory, factory, diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts index e95baec1a0a..fa58b182002 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager.ts @@ -11,9 +11,9 @@ import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { IFileWorkingCopy, IFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; -export interface IBaseFileWorkingCopyManager> extends IDisposable { +export interface IBaseFileWorkingCopyManager> extends IDisposable { /** * An event for when a file working copy was created. @@ -44,7 +44,7 @@ export interface IBaseFileWorkingCopyManager; } -export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { +export abstract class BaseFileWorkingCopyManager> extends Disposable implements IBaseFileWorkingCopyManager { private readonly _onDidCreate = this._register(new Emitter()); readonly onDidCreate = this._onDidCreate.event; diff --git a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts similarity index 81% rename from src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts rename to src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index b0a95a39ff9..141dd0596bb 100644 --- a/src/vs/workbench/services/workingCopy/common/abstractFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -10,10 +10,11 @@ import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; -export interface IBaseFileWorkingCopyModelFactory { +export interface IFileWorkingCopyModelFactory { /** - * Create a model from the given content under the provided resource. + * Create a model for the untitled or stored working copy + * from the given content under the provided resource. * * @param resource the `URI` of the model * @param contents the content of the model to create it @@ -24,15 +25,15 @@ export interface IBaseFileWorkingCopyModelFactory; } -export interface IBaseFileWorkingCopy extends IWorkingCopy, IDisposable { +export interface IFileWorkingCopy extends IWorkingCopy, IDisposable { /** * An event for when the file working copy has been reverted. @@ -100,10 +101,10 @@ export interface IBaseFileWorkingCopy exten /** * Whether we have a resolved model or not. */ - isResolved(): this is IBaseResolvedFileWorkingCopy; + isResolved(): this is IResolvedFileWorkingCopy; } -export interface IBaseResolvedFileWorkingCopy extends IBaseFileWorkingCopy { +export interface IResolvedFileWorkingCopy extends IFileWorkingCopy { /** * A resolved file working copy has a resolved model. diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts similarity index 96% rename from src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts rename to src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 8c256e0246a..2447d8a3451 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager2.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -24,7 +24,7 @@ import { INewOrExistingUntitledFileWorkingCopyOptions, INewUntitledFileWorkingCo import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { isValidBasename } from 'vs/base/common/extpath'; import { IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; -import { IBaseFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { IFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -37,7 +37,7 @@ import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/com import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export interface IFileWorkingCopyManager2 extends IBaseFileWorkingCopyManager> { +export interface IFileWorkingCopyManager extends IBaseFileWorkingCopyManager> { /** * Provides access to the manager for stored file working copies. @@ -129,9 +129,9 @@ export interface IFileWorkingCopySaveAsOptions extends ISaveOptions { suggestedTarget?: URI; } -export class FileWorkingCopyManager2 extends Disposable implements IFileWorkingCopyManager2 { +export class FileWorkingCopyManager extends Disposable implements IFileWorkingCopyManager { - readonly onDidCreate: Event>; + readonly onDidCreate: Event>; readonly stored: IStoredFileWorkingCopyManager; readonly untitled: IUntitledFileWorkingCopyManager; @@ -166,7 +166,7 @@ export class FileWorkingCopyManager2>(this.stored.onDidCreate, this.untitled.onDidCreate); + this.onDidCreate = Event.any>(this.stored.onDidCreate, this.untitled.onDidCreate); } //#region get / get all diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index ee6091e2297..8a56b821263 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -27,12 +27,12 @@ import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/com import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; import { IResourceWorkingCopy, ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; -import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel, IBaseFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; /** * Stored file specific working copy model factory. */ -export interface IStoredFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } +export interface IStoredFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } /** * The underlying model of a stored file working copy provides some @@ -40,7 +40,7 @@ export interface IStoredFileWorkingCopyModelFactory; @@ -89,7 +89,7 @@ export interface IStoredFileWorkingCopyModelContentChangedEvent { * of functionality can be built on top, such as saving in * a secure way to prevent data loss. */ -export interface IStoredFileWorkingCopy extends IResourceWorkingCopy, IBaseFileWorkingCopy { +export interface IStoredFileWorkingCopy extends IResourceWorkingCopy, IFileWorkingCopy { /** * An event for when a stored file working copy was resolved. diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index 567bb245767..24b22e5681a 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -21,7 +21,6 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { CancellationToken } from 'vs/base/common/cancellation'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { BaseFileWorkingCopyManager, IBaseFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopyManager'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; @@ -88,13 +87,6 @@ export interface IStoredFileWorkingCopyManager): true | Promise; - - - /** - * @deprecated - */ - saveAs(source: URI, target: URI): Promise | undefined>; - saveAs(source: URI, target: undefined): Promise | undefined>; } export interface IStoredFileWorkingCopySaveEvent { @@ -161,7 +153,6 @@ export class StoredFileWorkingCopyManager @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IFileDialogService private readonly fileDialogService: IFileDialogService, @ITextFileService private readonly textFileService: ITextFileService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @@ -552,101 +543,4 @@ export class StoredFileWorkingCopyManager } //#endregion - - - //#region Save As... - - /** - * @deprecated TODO@bpasero remove - */ - async saveAs(source: URI, target?: URI): Promise | undefined> { - - // If not provided, ask user for target - if (!target) { - target = await this.fileDialogService.pickFileToSave(source); - - if (!target) { - return undefined; // user canceled - } - } - - // Do it - return this.doSaveAs(source, target); - } - - private async doSaveAs(source: URI, target: URI): Promise | undefined> { - let sourceContents: VSBufferReadableStream; - - // If the source is an existing stored file working copy, we can directly - // use that to copy the contents to the target destination - const sourceWorkingCopy = this.get(source); - if (sourceWorkingCopy?.isResolved()) { - sourceContents = await sourceWorkingCopy.model.snapshot(CancellationToken.None); - } - - // Otherwise we resolve the contents from the underlying file - else { - sourceContents = (await this.fileService.readFileStream(source)).value; - } - - // Save the contents through working copy to benefit from save - // participants and handling a potential already existing target - return this.doSaveAsWorkingCopy(source, sourceContents, target); - } - - private async doSaveAsWorkingCopy(source: URI, sourceContents: VSBufferReadableStream, target: URI): Promise> { - - // Prefer an existing working copy if it is already resolved - // for the given target resource - let targetExists = false; - let targetWorkingCopy = this.get(target); - if (targetWorkingCopy?.isResolved()) { - targetExists = true; - } - - // Otherwise create the target working copy empty if - // it does not exist already and resolve it from there - else { - targetExists = await this.fileService.exists(target); - - // Create target file adhoc if it does not exist yet - if (!targetExists) { - await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); - } - - // At this point we need to resolve the target working copy - // and we have to do an explicit check if the source URI - // equals the target via URI identity. If they match and we - // have had an existing working copy with the source, we - // prefer that one over resolving the target. Otherwiese we - // would potentially introduce a - if (this.uriIdentityService.extUri.isEqual(source, target) && this.get(source)) { - targetWorkingCopy = await this.resolve(source); - } else { - targetWorkingCopy = await this.resolve(target); - } - } - - // Take over content from source to target - await targetWorkingCopy.model?.update(sourceContents, CancellationToken.None); - - // Save target - await targetWorkingCopy.save({ force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ }); - - // Revert the source - await this.doRevert(source); - - return targetWorkingCopy; - } - - private async doRevert(resource: URI): Promise { - const workingCopy = this.get(resource); - if (!workingCopy) { - return undefined; - } - - return workingCopy.revert(); - } - - //#endregion } diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index c41f65dc8c0..2ce885dc70b 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -6,7 +6,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { IBaseFileWorkingCopy, IBaseFileWorkingCopyModel, IBaseFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/abstractFileWorkingCopy'; +import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -21,7 +21,7 @@ import { emptyStream } from 'vs/base/common/stream'; /** * Untitled file specific working copy model factory. */ -export interface IUntitledFileWorkingCopyModelFactory extends IBaseFileWorkingCopyModelFactory { } +export interface IUntitledFileWorkingCopyModelFactory extends IFileWorkingCopyModelFactory { } /** * The underlying model of a untitled file working copy provides @@ -29,7 +29,7 @@ export interface IUntitledFileWorkingCopyModelFactory; } @@ -45,7 +45,7 @@ export interface IUntitledFileWorkingCopyModelContentChangedEvent { readonly isEmpty: boolean; } -export interface IUntitledFileWorkingCopy extends IBaseFileWorkingCopy { +export interface IUntitledFileWorkingCopy extends IFileWorkingCopy { /** * Whether this untitled file working copy model has an associated file path. diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts similarity index 96% rename from src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts rename to src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index c89ef474196..96c7401fd61 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager2.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -11,16 +11,16 @@ import { StoredFileWorkingCopy, IStoredFileWorkingCopy } from 'vs/workbench/serv import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { TestStoredFileWorkingCopyModel, TestStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test'; import { Schemas } from 'vs/base/common/network'; -import { IFileWorkingCopyManager2, FileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; +import { IFileWorkingCopyManager, FileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; import { UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; -suite('FileWorkingCopyManager2', () => { +suite('FileWorkingCopyManager', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let manager: IFileWorkingCopyManager2; + let manager: IFileWorkingCopyManager; setup(() => { instantiationService = workbenchInstantiationService(); @@ -29,8 +29,8 @@ suite('FileWorkingCopyManager2', () => { accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); - manager = new FileWorkingCopyManager2( - 'testFileWorkingCopyType2', + manager = new FileWorkingCopyManager( + 'testFileWorkingCopyType', new TestStoredFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts index 26fe7bdafca..87afff8dd9e 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -30,7 +30,7 @@ suite('StoredFileWorkingCopyManager', () => { 'testStoredFileWorkingCopyType', new TestStoredFileWorkingCopyModelFactory(), accessor.fileService, accessor.lifecycleService, accessor.labelService, accessor.logService, - accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.fileDialogService, + accessor.workingCopyFileService, accessor.workingCopyBackupService, accessor.uriIdentityService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyService, accessor.notificationService, accessor.workingCopyEditorService, accessor.editorService, accessor.elevatedFileService ); @@ -447,122 +447,6 @@ suite('StoredFileWorkingCopyManager', () => { assert.strictEqual(sourceWorkingCopy.model?.contents, 'hello move'); }); - // saveAs: unresolved source, unresolved target - - test('saveAs (same target, unresolved source, unresolved target)', () => { - const source = URI.file('/path/source.txt'); - - return testSaveAs(source, source, false, false); - }); - - test('saveAs (same target, different case, unresolved source, unresolved target)', async () => { - const source = URI.file('/path/source.txt'); - const target = URI.file('/path/SOURCE.txt'); - - return testSaveAs(source, target, false, false); - }); - - test('saveAs (different target, unresolved source, unresolved target)', async () => { - const source = URI.file('/path/source.txt'); - const target = URI.file('/path/target.txt'); - - return testSaveAs(source, target, false, false); - }); - - // saveAs: resolved source, unresolved target - - test('saveAs (same target, resolved source, unresolved target)', () => { - const source = URI.file('/path/source.txt'); - - return testSaveAs(source, source, true, false); - }); - - test('saveAs (same target, different case, resolved source, unresolved target)', async () => { - const source = URI.file('/path/source.txt'); - const target = URI.file('/path/SOURCE.txt'); - - return testSaveAs(source, target, true, false); - }); - - test('saveAs (different target, resolved source, unresolved target)', async () => { - const source = URI.file('/path/source.txt'); - const target = URI.file('/path/target.txt'); - - return testSaveAs(source, target, true, false); - }); - - // saveAs: unresolved source, resolved target - - test('saveAs (same target, unresolved source, resolved target)', () => { - const source = URI.file('/path/source.txt'); - - return testSaveAs(source, source, false, true); - }); - - test('saveAs (same target, different case, unresolved source, resolved target)', async () => { - const source = URI.file('/path/source.txt'); - const target = URI.file('/path/SOURCE.txt'); - - return testSaveAs(source, target, false, true); - }); - - test('saveAs (different target, unresolved source, resolved target)', async () => { - const source = URI.file('/path/source.txt'); - const target = URI.file('/path/target.txt'); - - return testSaveAs(source, target, false, true); - }); - - // saveAs: resolved source, resolved target - - test('saveAs (same target, resolved source, resolved target)', () => { - const source = URI.file('/path/source.txt'); - - return testSaveAs(source, source, true, true); - }); - - test('saveAs (different target, resolved source, resolved target)', async () => { - const source = URI.file('/path/source.txt'); - const target = URI.file('/path/target.txt'); - - return testSaveAs(source, target, true, true); - }); - - async function testSaveAs(source: URI, target: URI, resolveSource: boolean, resolveTarget: boolean) { - let sourceWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; - if (resolveSource) { - sourceWorkingCopy = await manager.resolve(source); - sourceWorkingCopy.model?.updateContents('hello world'); - assert.ok(sourceWorkingCopy.isDirty()); - } - - let targetWorkingCopy: IStoredFileWorkingCopy | undefined = undefined; - if (resolveTarget) { - targetWorkingCopy = await manager.resolve(target); - targetWorkingCopy.model?.updateContents('hello world'); - assert.ok(targetWorkingCopy.isDirty()); - } - - const result = await manager.saveAs(source, target); - if (accessor.uriIdentityService.extUri.isEqual(source, target) && resolveSource) { - // if the uris are considered equal (different case on macOS/Windows) - // and the source is to be resolved, the resulting working copy resource - // will be the source resource because we consider file working copies - // the same in that case - assert.strictEqual(source.toString(), result?.resource.toString()); - } else { - assert.strictEqual(target.toString(), result?.resource.toString()); - } - - if (resolveSource) { - assert.strictEqual(sourceWorkingCopy?.isDirty(), false); - } - - if (resolveTarget) { - assert.strictEqual(targetWorkingCopy?.isDirty(), false); - } - } - test('canDispose with dirty working copy', async () => { const resource = URI.file('/path/index_something.txt'); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 9cf2139f310..0838e0c74d3 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -8,7 +8,7 @@ import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { FileWorkingCopyManager2, IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; +import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { TestStoredFileWorkingCopyModel, TestStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test'; import { TestUntitledFileWorkingCopyModel, TestUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; @@ -19,7 +19,7 @@ suite('UntitledFileWorkingCopyManager', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; - let manager: IFileWorkingCopyManager2; + let manager: IFileWorkingCopyManager; setup(() => { instantiationService = workbenchInstantiationService(); @@ -28,7 +28,7 @@ suite('UntitledFileWorkingCopyManager', () => { accessor.fileService.registerProvider(Schemas.file, new TestInMemoryFileSystemProvider()); accessor.fileService.registerProvider(Schemas.vscodeRemote, new TestInMemoryFileSystemProvider()); - manager = new FileWorkingCopyManager2( + manager = new FileWorkingCopyManager( 'testUntitledFileWorkingCopyType', new TestStoredFileWorkingCopyModelFactory(), new TestUntitledFileWorkingCopyModelFactory(), From 703ca68ea6bee996ffc3d646da843bcb085cce3c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 08:39:26 +0200 Subject: [PATCH 239/270] fix typo --- .../contrib/notebook/common/notebookEditorModel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index ccc0da191b1..b0684bbf13f 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -462,11 +462,11 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } isOrphaned(): boolean { - return SimpleNotebookEditorModel._isStordFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN); + return SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(StoredFileWorkingCopyState.ORPHAN); } isReadonly(): boolean { - if (SimpleNotebookEditorModel._isStordFileWorkingCopy(this._workingCopy)) { + if (SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy)) { return this._workingCopy.isReadonly(); } else if (this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { return true; @@ -518,7 +518,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE return this._instantiationService.createInstance(NotebookEditorInput, newWorkingCopy.resource, this.viewType, {}); } - private static _isStordFileWorkingCopy(candidate?: unknown): candidate is IStoredFileWorkingCopy { + private static _isStoredFileWorkingCopy(candidate?: unknown): candidate is IStoredFileWorkingCopy { return candidate instanceof StoredFileWorkingCopy; } } From 43b51ce8e765e01c25668f83737e4aa960ecb9a0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 08:50:03 +0200 Subject: [PATCH 240/270] notebooks - fix type identifier compute --- src/vs/workbench/contrib/notebook/common/notebookCommon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 52c6227d7e4..560db334bc9 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -922,7 +922,7 @@ export class NotebookWorkingCopyTypeIdentifier { private static _prefix = 'notebook/'; static create(viewType: string): string { - return `${NotebookWorkingCopyTypeIdentifier._prefix}/${viewType}`; + return `${NotebookWorkingCopyTypeIdentifier._prefix}${viewType}`; } static parse(candidate: string): string | undefined { From 17459a4abd16a001394dd4fd96abe702506149da Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 08:52:59 +0200 Subject: [PATCH 241/270] notebooks - add test for NotebookWorkingCopyTypeIdentifier --- .../contrib/notebook/test/notebookCommon.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts index f431f6cd4dd..88da6bd5e79 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, CellKind, diff, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, CellKind, diff, CellUri, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { cellRangesToIndexes, cellIndexesToRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { TestCell, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { URI } from 'vs/base/common/uri'; @@ -323,3 +323,13 @@ suite('CellRange', function () { assert.deepStrictEqual(cellIndexesToRanges([10, 9]), [{ start: 9, end: 11 }]); }); }); + +suite('NotebookWorkingCopyTypeIdentifier', function () { + + test('works', function () { + const viewType = 'testViewType'; + const type = NotebookWorkingCopyTypeIdentifier.create('testViewType'); + assert.strictEqual(NotebookWorkingCopyTypeIdentifier.parse(type), viewType); + assert.strictEqual(NotebookWorkingCopyTypeIdentifier.parse('something'), undefined); + }); +}); From 4b5db9098c251f0582dc60d478f80f228020559d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 09:12:01 +0200 Subject: [PATCH 242/270] smoke test - disable failing search test //cc @JacksonKearl --- test/smoke/src/areas/search/search.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 72ec498f988..4eca396976e 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -16,7 +16,7 @@ export function setup() { }); // https://github.com/microsoft/vscode/issues/124146 - it('has a tooltp with a keybinding', async function () { + it.skip /* https://github.com/microsoft/vscode/issues/124335 */('has a tooltp with a keybinding', async function () { const app = this.app as Application; const tooltip: string = await app.workbench.search.getSearchTooltip(); if (!/Search \(.+\)/.test(tooltip)) { From 779f9876bc0a29fa2cfd811e5c613e05c5ec8e21 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 May 2021 10:44:28 +0200 Subject: [PATCH 243/270] debug: properly read the debug.saveBeforeStart respecting activeEditorMode --- src/vs/workbench/contrib/debug/browser/debugService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 29ecf50f63f..a0c444ab666 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -285,8 +285,7 @@ export class DebugService implements IDebugService { // make sure to save all files and that the configuration is up to date await this.extensionService.activateByEvent('onDebug'); if (!options?.parentSession) { - const saveBeforeStartConfig: string = this.configurationService.getValue('debug.saveBeforeStart'); - + const saveBeforeStartConfig: string = this.configurationService.getValue('debug.saveBeforeStart', { overrideIdentifier: this.editorService.activeTextEditorMode }); if (saveBeforeStartConfig !== 'none') { await this.editorService.saveAll(); if (saveBeforeStartConfig === 'allEditorsInActiveGroup') { From 73c6f34f9ecaf6a396463e4fd0a644837688e95f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 21 May 2021 11:20:18 +0200 Subject: [PATCH 244/270] Do not recognize Debian's .install file as a shell script. Fixes #124295 --- extensions/shellscript/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/shellscript/package.json b/extensions/shellscript/package.json index a8ecba9b104..3e1e3f3e9c8 100644 --- a/extensions/shellscript/package.json +++ b/extensions/shellscript/package.json @@ -32,7 +32,6 @@ ".bash_profile", ".bash_login", ".ebuild", - ".install", ".profile", ".bash_logout", ".xprofile", From a6f7aa5e4c5832c3d788ab9a8146ac69a6e5a77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 21 May 2021 11:23:45 +0200 Subject: [PATCH 245/270] handle workspace uris related to #124263 --- src/vs/code/electron-main/app.ts | 8 +++ .../editor/browser/services/openerService.ts | 28 ++++++--- .../browser/services/openerService.test.ts | 21 +++++++ src/vs/platform/opener/common/opener.ts | 1 + src/vs/vscode.proposed.d.ts | 63 +++++++++++++++++++ .../workbench/api/browser/mainThreadWindow.ts | 3 +- .../workbench/api/common/extHost.api.impl.ts | 6 ++ src/vs/workbench/api/common/extHostWindow.ts | 2 - .../contrib/remote/common/tunnelFactory.ts | 13 +++- src/vs/workbench/electron-sandbox/window.ts | 15 +++++ 10 files changed, 145 insertions(+), 15 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index a615da5cccf..630c577a640 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -717,8 +717,16 @@ export class CodeApplication extends Disposable { // protocol invocations outside of VSCode. const app = this; const environmentService = this.environmentMainService; + const productService = this.productService; urlService.registerHandler({ async handleURL(uri: URI, options?: IOpenURLOptions): Promise { + if (uri.scheme === productService.urlProtocol && uri.path === 'workspace') { + uri = uri.with({ + authority: 'file', + path: URI.parse(uri.query).path, + query: '' + }); + } // If URI should be blocked, behave as if it's handled if (app.shouldBlockURI(uri)) { diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index bc54879c7dc..0a3b2796127 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -191,31 +191,41 @@ export class OpenerService implements IOpenerService { async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise { for (const resolver of this._resolvers) { - const result = await resolver.resolveExternalUri(resource, options); - if (result) { - if (!this._resolvedUriTargets.has(result.resolved)) { - this._resolvedUriTargets.set(result.resolved, resource); + try { + const result = await resolver.resolveExternalUri(resource, options); + if (result) { + if (!this._resolvedUriTargets.has(result.resolved)) { + this._resolvedUriTargets.set(result.resolved, resource); + } + return result; } - return result; + } catch { + // noop } } - return { resolved: resource, dispose: () => { } }; + throw new Error('Could not resolve external URI: ' + resource.toString()); } private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise { //todo@jrieken IExternalUriResolver should support `uri: URI | string` const uri = typeof resource === 'string' ? URI.parse(resource) : resource; - const { resolved } = await this.resolveExternalUri(uri, options); + let externalUri: URI; + + try { + externalUri = (await this.resolveExternalUri(uri, options)).resolved; + } catch { + externalUri = uri; + } let href: string; - if (typeof resource === 'string' && uri.toString() === resolved.toString()) { + if (typeof resource === 'string' && uri.toString() === externalUri.toString()) { // open the url-string AS IS href = resource; } else { // open URI using the toString(noEncode)+encodeURI-trick - href = encodeURI(resolved.toString(true)); + href = encodeURI(externalUri.toString(true)); } if (options?.allowContributedOpeners) { diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 08c904f0764..ffcb441ad89 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -245,4 +245,25 @@ suite('OpenerService', function () { assert.ok(!matchesScheme(URI.parse('htt://microsoft.com'), 'http')); assert.ok(!matchesScheme(URI.parse('z://microsoft.com'), 'http')); }); + + test('resolveExternalUri', async function () { + const openerService = new OpenerService(editorService, NullCommandService); + + try { + await openerService.resolveExternalUri(URI.parse('file:///Users/user/folder')); + assert.fail('Should not reach here'); + } catch { + // OK + } + + const disposable = openerService.registerExternalUriResolver({ + async resolveExternalUri(uri) { + return { resolved: uri, dispose() { } }; + } + }); + + const result = await openerService.resolveExternalUri(URI.parse('file:///Users/user/folder')); + assert.deepStrictEqual(result.resolved.toString(), 'file:///Users/user/folder'); + disposable.dispose(); + }); }); diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 33c465736b7..fad7d23c761 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -109,6 +109,7 @@ export interface IOpenerService { /** * Resolve a resource to its external form. + * @throws whenever resolvers couldn't resolve this resource externally. */ resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 1a7103d51fe..522ad85e61c 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -3190,6 +3190,69 @@ declare module 'vscode' { //#endregion + //#region @joaomoreno https://github.com/microsoft/vscode/issues/124263 + // This API change only affects behavior and documentation, not API surface. + + namespace env { + + /** + * Resolves a uri to form that is accessible externally. + * + * #### `http:` or `https:` scheme + * + * Resolves an *external* uri, such as a `http:` or `https:` link, from where the extension is running to a + * uri to the same resource on the client machine. + * + * This is a no-op if the extension is running on the client machine. + * + * If the extension is running remotely, this function automatically establishes a port forwarding tunnel + * from the local machine to `target` on the remote and returns a local uri to the tunnel. The lifetime of + * the port forwarding tunnel is managed by VS Code and the tunnel can be closed by the user. + * + * *Note* that uris passed through `openExternal` are automatically resolved and you should not call `asExternalUri` on them. + * + * #### `vscode.env.uriScheme` + * + * Creates a uri that - if opened in a browser (e.g. via `openExternal`) - will result in a registered {@link UriHandler} + * to trigger. + * + * Extensions should not make any assumptions about the resulting uri and should not alter it in anyway. + * Rather, extensions can e.g. use this uri in an authentication flow, by adding the uri as callback query + * argument to the server to authenticate to. + * + * *Note* that if the server decides to add additional query parameters to the uri (e.g. a token or secret), it + * will appear in the uri that is passed to the {@link UriHandler}. + * + * **Example** of an authentication flow: + * ```typescript + * vscode.window.registerUriHandler({ + * handleUri(uri: vscode.Uri): vscode.ProviderResult { + * if (uri.path === '/did-authenticate') { + * console.log(uri.toString()); + * } + * } + * }); + * + * const callableUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://my.extension/did-authenticate`)); + * await vscode.env.openExternal(callableUri); + * ``` + * + * *Note* that extensions should not cache the result of `asExternalUri` as the resolved uri may become invalid due to + * a system or user action — for example, in remote cases, a user may close a port forwarding tunnel that was opened by + * `asExternalUri`. + * + * #### Any other scheme + * + * Any other scheme will be handled as if the provided URI is a workspace URI. In that case, the method will return + * a URI which, when handled, will make VS Code open the workspace. + * + * @return A uri that can be used on the client machine. + */ + export function asExternalUri(target: Uri): Thenable; + } + + //#endregion + //#region https://github.com/Microsoft/vscode/issues/15178 // TODO@API must be a class diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 7f10aa334c6..5dc01ce8c76 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -60,8 +60,7 @@ export class MainThreadWindow implements MainThreadWindowShape { } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { - const uri = URI.revive(uriComponents); - const result = await this.openerService.resolveExternalUri(uri, options); + const result = await this.openerService.resolveExternalUri(URI.revive(uriComponents), options); return result.resolved; } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 27c01093e5c..ad6422e744a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -87,6 +87,8 @@ import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookK import { RemoteTrustOption } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers'; +import { Schemas } from 'vs/base/common/network'; +import { matchesScheme } from 'vs/platform/opener/common/opener'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -320,6 +322,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostUrls.createAppUri(uri); } + if (!matchesScheme(uri.scheme, Schemas.http) && !matchesScheme(uri.scheme, Schemas.https)) { + checkProposedApiEnabled(extension); // https://github.com/microsoft/vscode/issues/124263 + } + return extHostWindow.asExternalUri(uri, { allowTunneling: !!initData.remote.authority }); }, get remoteName() { diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index a8c05964b01..46e6191675f 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -61,8 +61,6 @@ export class ExtHostWindow implements ExtHostWindowShape { async asExternalUri(uri: URI, options: IOpenUriOptions): Promise { if (isFalsyOrWhitespace(uri.scheme)) { return Promise.reject('Invalid scheme - cannot be empty'); - } else if (!new Set([Schemas.http, Schemas.https]).has(uri.scheme)) { - return Promise.reject(`Invalid scheme '${uri.scheme}'`); } const result = await this._proxy.$asExternalUri(uri, options); diff --git a/src/vs/workbench/contrib/remote/common/tunnelFactory.ts b/src/vs/workbench/contrib/remote/common/tunnelFactory.ts index 36524ca4b7c..36a3cf1f914 100644 --- a/src/vs/workbench/contrib/remote/common/tunnelFactory.ts +++ b/src/vs/workbench/contrib/remote/common/tunnelFactory.ts @@ -13,10 +13,11 @@ import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remo import { ILogService } from 'vs/platform/log/common/log'; export class TunnelFactoryContribution extends Disposable implements IWorkbenchContribution { + constructor( @ITunnelService tunnelService: ITunnelService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IOpenerService openerService: IOpenerService, + @IOpenerService private openerService: IOpenerService, @IRemoteExplorerService remoteExplorerService: IRemoteExplorerService, @ILogService logService: ILogService ) { @@ -51,7 +52,7 @@ export class TunnelFactoryContribution extends Disposable implements IWorkbenchC tunnelRemoteHost: tunnel.remoteAddress.host, // The tunnel factory may give us an inaccessible local address. // To make sure this doesn't happen, resolve the uri immediately. - localAddress: (await openerService.resolveExternalUri(URI.parse(localAddress))).resolved.toString(), + localAddress: await this.resolveExternalUri(localAddress), public: !!tunnel.public, dispose: async () => { await tunnel.dispose(); } }; @@ -62,4 +63,12 @@ export class TunnelFactoryContribution extends Disposable implements IWorkbenchC remoteExplorerService.setTunnelInformation(undefined); } } + + private async resolveExternalUri(uri: string): Promise { + try { + return (await this.openerService.resolveExternalUri(URI.parse(uri))).resolved.toString(); + } catch { + return uri; + } + } } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 7754e9cdb0e..2035ba6adce 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -525,6 +525,21 @@ export class NativeWindow extends Disposable { } } } + + // Assume `uri` this is a workspace uri, let's see if we can handle it + await this.fileService.activateProvider(uri.scheme); + + if (this.fileService.canHandleResource(uri)) { + return { + resolved: URI.from({ + scheme: this.productService.urlProtocol, + path: 'workspace', + query: uri.toString() + }), + dispose() { } + }; + } + return undefined; } }); From f19843b0a3e508d613f5b1b43d0de50cd83a3ac8 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 11:33:53 +0200 Subject: [PATCH 246/270] editors - some :lipstick: and tests for workspace trust in openEditors --- .../services/editor/browser/editorService.ts | 134 ++++++++---------- .../services/editor/common/editorService.ts | 5 - .../editor/test/browser/editorService.test.ts | 73 +++++++++- .../test/common/testWorkspaceTrustService.ts | 8 +- .../test/browser/workbenchTestServices.ts | 7 +- 5 files changed, 143 insertions(+), 84 deletions(-) diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index d50c4165325..d5ac0c9f7bb 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -17,7 +17,7 @@ import { URI } from 'vs/base/common/uri'; import { basename, joinPath, isEqual } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorOverrideEntry, SidebySideURI } from 'vs/workbench/services/editor/common/editorService'; +import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { coalesce, distinct, firstOrDefault, insert } from 'vs/base/common/arrays'; @@ -39,7 +39,6 @@ import { ContributedEditorPriority, DEFAULT_EDITOR_ASSOCIATION, IEditorOverrideS import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IFileToOpen } from 'vs/platform/windows/common/windows'; type CachedEditorInput = TextResourceEditorInput | IFileEditorInput | UntitledTextEditorInput; type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; @@ -619,9 +618,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Override handling: ask providers to override if (resolvedOptions?.override !== EditorOverride.DISABLED) { + // TODO@lramos15 this will get cleaned up soon, but since the override // service no longer uses the override flow we must check that const resolvedInputWithOptionsAndGroup = await this.editorOverrideService.resolveEditorOverride(resolvedEditor, resolvedOptions, resolvedGroup); + // If we didn't override try the legacy overrides if (!resolvedInputWithOptionsAndGroup || resolvedEditor.matches(resolvedInputWithOptionsAndGroup.editor)) { const override = this.doOverrideOpenEditor(resolvedEditor, resolvedOptions, resolvedGroup); @@ -792,83 +793,16 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#endregion - //#region Workspace Trust - - /** - * Given an array of typed or untyped editor inputs converts them to an array of resources - * @param editors The editor inputs - * @returns The array of resources - */ - private convertToResourceArray(editors: Array): Array { - const resources: Array = []; - editors.forEach(editor => { - if (isEditorInputWithOptions(editor)) { - if (editor.editor instanceof SideBySideEditorInput) { - if (!editor.editor.primary.resource || !editor.editor.secondary.resource) { - return; - } - resources.push({ - primary: editor.editor.primary.resource, - secondary: editor.editor.secondary.resource - }); - } - } - const resourceDiffEditor = editor as IResourceDiffEditorInput; - if (resourceDiffEditor.leftResource && resourceDiffEditor.rightResource) { - resources.push({ - primary: resourceDiffEditor.leftResource, - secondary: resourceDiffEditor.rightResource - }); - } - const otherEditors = editor as IResourceEditorInput | IUntitledTextResourceEditorInput; - if (otherEditors.resource) { - resources.push(otherEditors.resource); - } - }); - return resources; - } - - private isSideBySideURI(obj: unknown): obj is SidebySideURI { - const sideBySideURI: SidebySideURI = obj as SidebySideURI; - if (sideBySideURI.primary && sideBySideURI.secondary) { - return true; - } - return false; - } - /** - * Given a set of resources returns a boolean to indicate whether or not to proceed with opening - */ - private async handleWorkspaceTrust(editors: Array): Promise { - const resources = this.convertToResourceArray(editors); - const filteredResources: URI[] = []; - // Convert the array into a flat resource array - resources.forEach(r => { - if (this.isSideBySideURI(r)) { - filteredResources.push(r.primary); - filteredResources.push(r.secondary); - } else { - filteredResources.push(r); - } - }); - const trustResult = await this.workspaceTrustRequestService.requestOpenUris(filteredResources); - if (trustResult === WorkspaceTrustUriResponse.OpenInNewWindow) { - const filesToOpen: IFileToOpen[] = []; - filteredResources.forEach(resource => filesToOpen.push({ fileUri: resource })); - await this.hostService.openWindow(filesToOpen, { forceNewWindow: true }); - return false; - } - return trustResult === WorkspaceTrustUriResponse.Open; - } - - //#endregion - //#region openEditors() openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise; async openEditors(editors: Array, group?: OpenInEditorGroup): Promise { - if (!(await this.handleWorkspaceTrust(editors))) { + // Pass all editors to trust service to determine if + // we should proceed with opening the editors + const editorsTrusted = await this.handleWorkspaceTrust(editors); + if (!editorsTrusted) { return []; } @@ -934,6 +868,60 @@ export class EditorService extends Disposable implements EditorServiceImpl { return coalesce(await Promises.settled(result)); } + private async handleWorkspaceTrust(editors: Array): Promise { + const resources = this.extractEditorResources(editors); + + const trustResult = await this.workspaceTrustRequestService.requestOpenUris(resources); + switch (trustResult) { + case WorkspaceTrustUriResponse.Open: + return true; + case WorkspaceTrustUriResponse.OpenInNewWindow: + await this.hostService.openWindow(resources.map(resource => ({ fileUri: resource })), { forceNewWindow: true }); + return false; + case WorkspaceTrustUriResponse.Cancel: + return false; + } + } + + private extractEditorResources(editors: Array): URI[] { + const resources = new ResourceMap(); + + for (const editor of editors) { + + // Typed Editor + if (isEditorInputWithOptions(editor)) { + const resource = EditorResourceAccessor.getOriginalUri(editor.editor, { supportSideBySide: SideBySideEditor.BOTH }); + if (URI.isUri(resource)) { + resources.set(resource, true); + } else if (resource) { + if (resource.primary) { + resources.set(resource.primary, true); + } + + if (resource.secondary) { + resources.set(resource.secondary, true); + } + } + } + + // Untyped editor + else { + const resourceDiffEditor = editor as IResourceDiffEditorInput; + if (URI.isUri(resourceDiffEditor.leftResource) && URI.isUri(resourceDiffEditor.rightResource)) { + resources.set(resourceDiffEditor.leftResource, true); + resources.set(resourceDiffEditor.rightResource, true); + } + + const resourceEditor = editor as IResourceEditorInput; + if (URI.isUri(resourceEditor.resource)) { + resources.set(resourceEditor.resource, true); + } + } + } + + return Array.from(resources.keys()); + } + //#endregion //#region isOpened() diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 0f864f91702..cd4337732d1 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -16,11 +16,6 @@ export const IEditorService = createDecorator('editorService'); export type IResourceEditorInputType = IResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput; -export interface SidebySideURI { - primary: URI; - secondary: URI; -} - export interface IResourceEditorReplacement { readonly editor: IResourceEditorInputType; readonly replacement: IResourceEditorInputType; diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 0b1b0d3cf5d..9dc1b5c02a3 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -31,7 +31,7 @@ import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServic import { isLinux } from 'vs/base/common/platform'; import { MockScopableContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ContributedEditorPriority } from 'vs/workbench/services/editor/common/editorOverrideService'; -import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust'; import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; suite('EditorService', () => { @@ -230,6 +230,77 @@ suite('EditorService', () => { assert.strictEqual(part.activeGroup.getIndexOfEditor(replaceInput), 0); }); + test('openEditors() handles workspace trust (typed editors)', async () => { + const [part, service, accessor] = await createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openEditors'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + + const input3 = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('my://resource4-openEditors'), TEST_EDITOR_INPUT_ID); + const sideBySideInput = new SideBySideEditorInput('side by side', undefined, input3, input4); + + const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; + + try { + + // Trust: cancel + let trustEditorUris: URI[] = []; + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => { + trustEditorUris = uris; + return WorkspaceTrustUriResponse.Cancel; + }; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }]); + assert.strictEqual(part.activeGroup.count, 0); + assert.strictEqual(trustEditorUris.length, 4); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input1.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input2.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input3.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input4.resource.toString()), true); + + // Trust: open in new window + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => WorkspaceTrustUriResponse.OpenInNewWindow; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }]); + assert.strictEqual(part.activeGroup.count, 0); + + // Trust: allow + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => WorkspaceTrustUriResponse.Open; + + await service.openEditors([{ editor: input1 }, { editor: input2 }, { editor: sideBySideInput }]); + assert.strictEqual(part.activeGroup.count, 3); + } finally { + accessor.workspaceTrustRequestService.requestOpenUrisHandler = oldHandler; + } + }); + + test('openEditors() extracts proper resources from untyped editors for workspace trust', async () => { + const [part, service, accessor] = await createEditorService(); + + const input = { resource: URI.parse('my://resource-openEditors') }; + const otherInput = { leftResource: URI.parse('my://resource2-openEditors'), rightResource: URI.parse('my://resource3-openEditors') }; + + const oldHandler = accessor.workspaceTrustRequestService.requestOpenUrisHandler; + + try { + let trustEditorUris: URI[] = []; + accessor.workspaceTrustRequestService.requestOpenUrisHandler = async uris => { + trustEditorUris = uris; + return oldHandler(uris); + }; + + await service.openEditors([input, otherInput]); + assert.strictEqual(part.activeGroup.count, 0); + assert.strictEqual(trustEditorUris.length, 3); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === input.resource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === otherInput.leftResource.toString()), true); + assert.strictEqual(trustEditorUris.some(uri => uri.toString() === otherInput.rightResource.toString()), true); + } finally { + accessor.workspaceTrustRequestService.requestOpenUrisHandler = oldHandler; + } + }); + test('caching', function () { const instantiationService = workbenchInstantiationService(); const service = instantiationService.createInstance(EditorService); diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts index 083c35595c4..14282c50ac2 100644 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts @@ -87,8 +87,12 @@ export class TestWorkspaceTrustRequestService implements IWorkspaceTrustRequestS constructor(private readonly _trusted: boolean) { } - async requestOpenUris(uris: URI[]): Promise { - return Promise.resolve(WorkspaceTrustUriResponse.Open); + requestOpenUrisHandler = async (uris: URI[]) => { + return WorkspaceTrustUriResponse.Open; + }; + + requestOpenUris(uris: URI[]): Promise { + return this.requestOpenUrisHandler(uris); } cancelRequest(): void { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index dad91802038..16d6a5a907a 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -123,8 +123,8 @@ import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResource import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { IEnterWorkspaceResult, IRecent, IRecentlyOpened, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { TestWorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { ILocalTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -283,7 +283,8 @@ export class TestServiceAccessor { @INotificationService public notificationService: INotificationService, @IWorkingCopyEditorService public workingCopyEditorService: IWorkingCopyEditorService, @IInstantiationService public instantiationService: IInstantiationService, - @IElevatedFileService public elevatedFileService: IElevatedFileService + @IElevatedFileService public elevatedFileService: IElevatedFileService, + @IWorkspaceTrustRequestService public workspaceTrustRequestService: TestWorkspaceTrustRequestService ) { } } From 063be236f34c2ef37322ae157366e52624b1faee Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 May 2021 11:34:39 +0200 Subject: [PATCH 247/270] scm do not auto focus input box on iPad fixes #122044 --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index dfc17b3e07d..6eee9af184f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1239,7 +1239,7 @@ class ViewModel { } focus() { - if (this.tree.getFocus().length === 0) { + if (this.tree.getFocus().length === 0 && !platform.isIOS) { for (const repository of this.scmViewService.visibleRepositories) { const widget = this.inputRenderer.getRenderedInputWidget(repository.input); From 949f60498aecf6f78f63aa587999589a8f65c3ac Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 11:36:20 +0200 Subject: [PATCH 248/270] Merge pull request #124346 from microsoft/ben/pfs-promises fs - favor non promise based methods (fix #124176) --- src/vs/base/node/extpath.ts | 7 +- src/vs/base/node/pfs.ts | 93 ++++++++++++++----- src/vs/base/node/processes.ts | 3 +- src/vs/base/node/zip.ts | 8 +- src/vs/base/parts/storage/node/storage.ts | 9 +- .../parts/storage/test/node/storage.test.ts | 9 +- src/vs/base/test/node/crypto.test.ts | 5 +- src/vs/base/test/node/extpath.test.ts | 5 +- src/vs/base/test/node/pfs/pfs.test.ts | 24 ++--- src/vs/base/test/node/zip/zip.test.ts | 5 +- .../contrib/languagePackCachedDataCleaner.ts | 5 +- .../contrib/nodeCachedDataCleaner.ts | 5 +- .../contrib/storageDataCleaner.ts | 5 +- src/vs/code/electron-main/main.ts | 5 +- src/vs/code/node/cliProcessMain.ts | 5 +- .../backup/electron-main/backupMainService.ts | 6 +- .../electron-main/backupMainService.test.ts | 34 +++---- .../node/extensionDownloader.ts | 4 +- .../node/extensionManagementService.ts | 3 +- .../node/extensionsScanner.ts | 11 +-- .../files/node/diskFileSystemProvider.ts | 25 +++-- .../electron-browser/diskFileService.test.ts | 24 ++--- .../localizations/node/localizations.ts | 5 +- .../electron-main/nativeHostMainService.ts | 9 +- .../state/test/electron-main/state.test.ts | 6 +- .../storage/electron-main/storageMain.ts | 5 +- .../platform/terminal/node/terminalProcess.ts | 5 +- .../terminal/node/terminalProfiles.ts | 7 +- .../electron-main/updateService.win32.ts | 6 +- .../workspacesManagementMainService.ts | 6 +- .../workspacesManagementMainService.test.ts | 2 +- .../api/node/extHostOutputService.ts | 5 +- .../api/node/extHostTunnelService.ts | 11 +-- .../test/node/terminalProfiles.test.ts | 4 +- .../cachedExtensionScanner.ts | 11 +-- .../extensions/node/extensionPoints.ts | 11 +-- .../keyboardMapperTestUtils.ts | 7 +- .../electron-browser/commonProperties.test.ts | 4 +- .../nativeTextFileService.io.test.ts | 7 +- .../workingCopyBackupService.test.ts | 10 +- .../workingCopyBackupTracker.test.ts | 7 +- .../colorRegistry.releaseTest.ts | 3 +- 42 files changed, 229 insertions(+), 202 deletions(-) diff --git a/src/vs/base/node/extpath.ts b/src/vs/base/node/extpath.ts index eaf62cd8e12..325c0d2d1dc 100644 --- a/src/vs/base/node/extpath.ts +++ b/src/vs/base/node/extpath.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import { promisify } from 'util'; import { rtrim } from 'vs/base/common/strings'; import { sep, join, normalize, dirname, basename } from 'vs/base/common/path'; -import { readdirSync } from 'vs/base/node/pfs'; +import { Promises, readdirSync } from 'vs/base/node/pfs'; /** * Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83 @@ -57,7 +56,7 @@ export async function realpath(path: string): Promise { // calls `fs.native.realpath` which will result in subst // drives to be resolved to their target on Windows // https://github.com/microsoft/vscode/issues/118562 - return await promisify(fs.realpath)(path); + return await Promises.realpath(path); } catch (error) { // We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization @@ -67,7 +66,7 @@ export async function realpath(path: string): Promise { // to not resolve links but to simply see if the path is read accessible or not. const normalizedPath = normalizePath(path); - await fs.promises.access(normalizedPath, fs.constants.R_OK); + await Promises.access(normalizedPath, fs.constants.R_OK); return normalizedPath; } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 30d25a8cc33..40a81246b02 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import { tmpdir } from 'os'; +import { promisify } from 'util'; import { join } from 'vs/base/common/path'; import { ResourceQueue } from 'vs/base/common/async'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; @@ -56,7 +57,7 @@ async function rimrafMove(path: string): Promise { try { const pathInTemp = join(tmpdir(), generateUuid()); try { - await fs.promises.rename(path, pathInTemp); + await Promises.rename(path, pathInTemp); } catch (error) { return rimrafUnlink(path); // if rename fails, delete without tmp dir } @@ -71,7 +72,7 @@ async function rimrafMove(path: string): Promise { } async function rimrafUnlink(path: string): Promise { - return fs.promises.rmdir(path, { recursive: true, maxRetries: 3 }); + return Promises.rmdir(path, { recursive: true, maxRetries: 3 }); } export function rimrafSync(path: string): void { @@ -102,12 +103,12 @@ export interface IDirent { export async function readdir(path: string): Promise; export async function readdir(path: string, options: { withFileTypes: true }): Promise; export async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> { - return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path))); + return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : Promises.readdir(path))); } async function safeReaddirWithFileTypes(path: string): Promise { try { - return await fs.promises.readdir(path, { withFileTypes: true }); + return await Promises.readdir(path, { withFileTypes: true }); } catch (error) { console.warn('[node.js fs] readdir with filetypes failed with error: ', error); } @@ -126,7 +127,7 @@ async function safeReaddirWithFileTypes(path: string): Promise { let isSymbolicLink = false; try { - const lstat = await fs.promises.lstat(join(path, child)); + const lstat = await Promises.lstat(join(path, child)); isFile = lstat.isFile(); isDirectory = lstat.isDirectory(); @@ -251,7 +252,7 @@ export namespace SymlinkSupport { // First stat the link let lstats: fs.Stats | undefined; try { - lstats = await fs.promises.lstat(path); + lstats = await Promises.lstat(path); // Return early if the stat is not a symbolic link at all if (!lstats.isSymbolicLink()) { @@ -264,7 +265,7 @@ export namespace SymlinkSupport { // If the stat is a symbolic link or failed to stat, use fs.stat() // which for symbolic links will stat the target they point to try { - const stats = await fs.promises.stat(path); + const stats = await Promises.stat(path); return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; } catch (error) { @@ -279,7 +280,7 @@ export namespace SymlinkSupport { // are not supported (https://github.com/nodejs/node/issues/36790) if (isWindows && error.code === 'EACCES') { try { - const stats = await fs.promises.stat(await fs.promises.readlink(path)); + const stats = await Promises.stat(await Promises.readlink(path)); return { stat: stats, symbolicLink: { dangling: false } }; } catch (error) { @@ -488,24 +489,19 @@ export async function move(source: string, target: string): Promise { // as well because conceptually it is a change of a similar category. async function updateMtime(path: string): Promise { try { - const stat = await fs.promises.lstat(path); + const stat = await Promises.lstat(path); if (stat.isDirectory() || stat.isSymbolicLink()) { return; // only for files } - const fh = await fs.promises.open(path, 'a'); - try { - await fh.utimes(stat.atime, new Date()); - } finally { - await fh.close(); - } + await Promises.utimes(path, stat.atime, new Date()); } catch (error) { // Ignore any error } } try { - await fs.promises.rename(source, target); + await Promises.rename(source, target); await updateMtime(target); } catch (error) { @@ -594,7 +590,7 @@ async function doCopy(source: string, target: string, payload: ICopyPayload): Pr async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise { // Create folder - await fs.promises.mkdir(target, { recursive: true, mode }); + await Promises.mkdir(target, { recursive: true, mode }); // Copy each file recursively const files = await readdir(source); @@ -606,16 +602,16 @@ async function doCopyDirectory(source: string, target: string, mode: number, pay async function doCopyFile(source: string, target: string, mode: number): Promise { // Copy file - await fs.promises.copyFile(source, target); + await Promises.copyFile(source, target); // restore mode (https://github.com/nodejs/node/issues/1104) - await fs.promises.chmod(target, mode); + await Promises.chmod(target, mode); } async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise { // Figure out link target - let linkTarget = await fs.promises.readlink(source); + let linkTarget = await Promises.readlink(source); // Special case: the symlink points to a target that is // actually within the path that is being copied. In that @@ -626,7 +622,7 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo } // Create symlink - await fs.promises.symlink(linkTarget, target); + await Promises.symlink(linkTarget, target); } //#endregion @@ -635,7 +631,7 @@ async function doCopySymlink(source: string, target: string, payload: ICopyPaylo export async function exists(path: string): Promise { try { - await fs.promises.access(path); + await Promises.access(path); return true; } catch { @@ -644,3 +640,56 @@ export async function exists(path: string): Promise { } //#endregion + +//#region Promise based fs methods + +/** + * Note: prefer this namespace over the `fs.promises` API + * to enable `graceful-fs` to function properly. Given issue + * https://github.com/isaacs/node-graceful-fs/issues/160 it + * is evident that the module only takes care of the non-promise + * based fs methods. + * + * Another reason is `realpath` being entirely different in + * the promise based implementation compared to the other + * one (https://github.com/microsoft/vscode/issues/118562) + */ +export namespace Promises { + export const access = promisify(fs.access); + + export const stat = promisify(fs.stat); + export const lstat = promisify(fs.lstat); + export const utimes = promisify(fs.utimes); + + export const read = promisify(fs.read); + export const readFile = promisify(fs.readFile); + + export const write = promisify(fs.write); + export const writeFile = promisify(fs.writeFile); + + export const appendFile = promisify(fs.appendFile); + + export const fdatasync = promisify(fs.fdatasync); + export const truncate = promisify(fs.truncate); + + export const rename = promisify(fs.rename); + export const copyFile = promisify(fs.copyFile); + + export const open = promisify(fs.open); + export const close = promisify(fs.close); + + export const symlink = promisify(fs.symlink); + export const readlink = promisify(fs.readlink); + + export const chmod = promisify(fs.chmod); + + export const readdir = promisify(fs.readdir); + export const mkdir = promisify(fs.mkdir); + + export const unlink = promisify(fs.unlink); + export const rmdir = promisify(fs.rmdir); + + export const realpath = promisify(fs.realpath); +} + +//#endregion diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index 363dc229f62..f1b1b398212 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import * as cp from 'child_process'; import * as nls from 'vs/nls'; @@ -458,7 +457,7 @@ export namespace win32 { async function fileExists(path: string): Promise { if (await pfs.exists(path)) { - return !((await fs.promises.stat(path)).isDirectory()); + return !((await pfs.Promises.stat(path)).isDirectory()); } return false; } diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 0d74501d997..3c289c297d1 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -5,10 +5,10 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import { promises, createWriteStream, WriteStream } from 'fs'; +import { createWriteStream, WriteStream } from 'fs'; import { Readable } from 'stream'; import { Sequencer, createCancelablePromise } from 'vs/base/common/async'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises, rimraf } from 'vs/base/node/pfs'; import { open as _openZip, Entry, ZipFile } from 'yauzl'; import * as yazl from 'yazl'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -86,7 +86,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa } }); - return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { + return Promise.resolve(Promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise((c, e) => { if (token.isCancellationRequested) { return; } @@ -149,7 +149,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok // directory file names end with '/' if (/\/$/.test(fileName)) { const targetFileName = path.join(targetPath, fileName); - last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); + last = createCancelablePromise(token => Promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e)); return; } diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index e497862ddaf..e79d41f9e0d 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import type { Database, Statement } from 'vscode-sqlite3'; -import { promises } from 'fs'; import { Event } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; import { mapToString, setToString } from 'vs/base/common/map'; import { basename } from 'vs/base/common/path'; -import { copy } from 'vs/base/node/pfs'; +import { copy, Promises } from 'vs/base/node/pfs'; import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage'; interface IDatabaseConnection { @@ -187,7 +186,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // Delete the existing DB. If the path does not exist or fails to // be deleted, we do not try to recover anymore because we assume // that the path is no longer writeable for us. - return promises.unlink(this.path).then(() => { + return Promises.unlink(this.path).then(() => { // Re-open the DB fresh return this.doConnect(this.path).then(recoveryConnection => { @@ -273,9 +272,9 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // folder is really not writeable for us. // try { - await promises.unlink(path); + await Promises.unlink(path); try { - await promises.rename(this.toBackupPath(path), path); + await Promises.rename(this.toBackupPath(path), path); } catch (error) { // ignore } diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index fd963590dc4..07846cf4a4f 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -7,9 +7,8 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/pa import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; import { strictEqual, ok } from 'assert'; -import { writeFile, exists, rimraf } from 'vs/base/node/pfs'; +import { writeFile, exists, rimraf, Promises } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { isWindows } from 'vs/base/common/platform'; @@ -23,7 +22,7 @@ flakySuite('Storage Library', function () { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(function () { @@ -296,7 +295,7 @@ flakySuite('SQLite Storage Library', function () { setup(function () { testdir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary'); - return promises.mkdir(testdir, { recursive: true }); + return Promises.mkdir(testdir, { recursive: true }); }); teardown(function () { @@ -477,7 +476,7 @@ flakySuite('SQLite Storage Library', function () { // on shutdown. await storage.checkIntegrity(true).then(null, error => { } /* error is expected here but we do not want to fail */); - await promises.unlink(backupPath); // also test that the recovery DB is backed up properly + await Promises.unlink(backupPath); // also test that the recovery DB is backed up properly let recoveryCalled = false; await storage.close(() => { diff --git a/src/vs/base/test/node/crypto.test.ts b/src/vs/base/test/node/crypto.test.ts index 67c188de8f0..f1b7ef70b94 100644 --- a/src/vs/base/test/node/crypto.test.ts +++ b/src/vs/base/test/node/crypto.test.ts @@ -6,8 +6,7 @@ import { checksum } from 'vs/base/node/crypto'; import { join } from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; -import { rimraf, writeFile } from 'vs/base/node/pfs'; +import { Promises, rimraf, writeFile } from 'vs/base/node/pfs'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; flakySuite('Crypto', () => { @@ -17,7 +16,7 @@ flakySuite('Crypto', () => { setup(function () { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(function () { diff --git a/src/vs/base/test/node/extpath.test.ts b/src/vs/base/test/node/extpath.test.ts index a1c8a7f94d2..0487ffcaebf 100644 --- a/src/vs/base/test/node/extpath.test.ts +++ b/src/vs/base/test/node/extpath.test.ts @@ -5,8 +5,7 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; -import { promises } from 'fs'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises, rimraf } from 'vs/base/node/pfs'; import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; @@ -16,7 +15,7 @@ flakySuite('Extpath', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 0eceb128b9a..2139f6cbe44 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; import { tmpdir } from 'os'; import { join, sep } from 'vs/base/common/path'; import { generateUuid } from 'vs/base/common/uuid'; -import { copy, exists, move, readdir, readDirsInDir, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs'; +import { copy, exists, move, Promises, readdir, readDirsInDir, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -22,7 +22,7 @@ flakySuite('PFS', function () { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); - return fs.promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { @@ -36,7 +36,7 @@ flakySuite('PFS', function () { await writeFile(testFile, 'Hello World', (null!)); - assert.strictEqual((await fs.promises.readFile(testFile)).toString(), 'Hello World'); + assert.strictEqual((await Promises.readFile(testFile)).toString(), 'Hello World'); }); test('writeFile - parallel write on different files works', async () => { @@ -200,7 +200,7 @@ flakySuite('PFS', function () { const id3 = generateUuid(); const copyTarget = join(testDir, id3); - await fs.promises.mkdir(symbolicLinkTarget, { recursive: true }); + await Promises.mkdir(symbolicLinkTarget, { recursive: true }); fs.symlinkSync(symbolicLinkTarget, symLink, 'junction'); @@ -217,7 +217,7 @@ flakySuite('PFS', function () { assert.ok(symbolicLink); assert.ok(!symbolicLink.dangling); - const target = await fs.promises.readlink(copyTarget); + const target = await Promises.readlink(copyTarget); assert.strictEqual(target, symbolicLinkTarget); // Copy does not preserve symlinks if configured as such @@ -253,7 +253,7 @@ flakySuite('PFS', function () { const sourceLinkTestFolder = join(sourceFolder, 'link-test'); // copy-test/link-test const sourceLinkMD5JSFolder = join(sourceLinkTestFolder, 'md5'); // copy-test/link-test/md5 const sourceLinkMD5JSFile = join(sourceLinkMD5JSFolder, 'md5.js'); // copy-test/link-test/md5/md5.js - await fs.promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); + await Promises.mkdir(sourceLinkMD5JSFolder, { recursive: true }); await writeFile(sourceLinkMD5JSFile, 'Hello from MD5'); const sourceLinkMD5JSFolderLinked = join(sourceLinkTestFolder, 'md5-linked'); // copy-test/link-test/md5-linked @@ -278,10 +278,10 @@ flakySuite('PFS', function () { assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked)); assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink()); - const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked); + const linkTarget = await Promises.readlink(targetLinkMD5JSFolderLinked); assert.strictEqual(linkTarget, targetLinkMD5JSFolder); - await fs.promises.rmdir(targetLinkTestFolder, { recursive: true }); + await Promises.rmdir(targetLinkTestFolder, { recursive: true }); } // Copy with `preserveSymlinks: false` and verify result @@ -315,7 +315,7 @@ flakySuite('PFS', function () { const id2 = generateUuid(); const symbolicLink = join(testDir, id2); - await fs.promises.mkdir(directory, { recursive: true }); + await Promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -334,7 +334,7 @@ flakySuite('PFS', function () { const id2 = generateUuid(); const symbolicLink = join(testDir, id2); - await fs.promises.mkdir(directory, { recursive: true }); + await Promises.mkdir(directory, { recursive: true }); fs.symlinkSync(directory, symbolicLink, 'junction'); @@ -350,7 +350,7 @@ flakySuite('PFS', function () { const id = generateUuid(); const newDir = join(testDir, 'pfs', id, 'öäü'); - await fs.promises.mkdir(newDir, { recursive: true }); + await Promises.mkdir(newDir, { recursive: true }); assert.ok(fs.existsSync(newDir)); @@ -362,7 +362,7 @@ flakySuite('PFS', function () { test('readdir (with file types)', async () => { if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { const newDir = join(testDir, 'öäü'); - await fs.promises.mkdir(newDir, { recursive: true }); + await Promises.mkdir(newDir, { recursive: true }); await writeFile(join(testDir, 'somefile.txt'), 'contents'); diff --git a/src/vs/base/test/node/zip/zip.test.ts b/src/vs/base/test/node/zip/zip.test.ts index 4f151af16e9..5e5470ce155 100644 --- a/src/vs/base/test/node/zip/zip.test.ts +++ b/src/vs/base/test/node/zip/zip.test.ts @@ -6,9 +6,8 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; import { tmpdir } from 'os'; -import { promises } from 'fs'; import { extract } from 'vs/base/node/zip'; -import { rimraf, exists } from 'vs/base/node/pfs'; +import { rimraf, exists, Promises } from 'vs/base/node/pfs'; import { createCancelablePromise } from 'vs/base/common/async'; import { getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils'; @@ -19,7 +18,7 @@ suite('Zip', () => { setup(() => { testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip'); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts index 00df213a37c..b76d7668802 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -55,7 +54,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { this._logService.info('Starting to clean up unused language packs.'); try { const installed: IStringDictionary = Object.create(null); - const metaData: LanguagePackFile = JSON.parse(await fs.promises.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8')); + const metaData: LanguagePackFile = JSON.parse(await pfs.Promises.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8')); for (let locale of Object.keys(metaData)) { const entry = metaData[locale]; installed[`${entry.hash}.${locale}`] = true; @@ -83,7 +82,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { continue; } const candidate = path.join(folder, entry); - const stat = await fs.promises.stat(candidate); + const stat = await pfs.Promises.stat(candidate); if (stat.isDirectory()) { const diff = now - stat.mtime.getTime(); if (diff > this._DataMaxAge) { diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts index fdb9aa7bb40..a6afc697617 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; import { basename, dirname, join } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { readdir, rimraf } from 'vs/base/node/pfs'; +import { Promises, readdir, rimraf } from 'vs/base/node/pfs'; import { IProductService } from 'vs/platform/product/common/productService'; export class NodeCachedDataCleaner { @@ -56,7 +55,7 @@ export class NodeCachedDataCleaner { if (entry !== nodeCachedDataCurrent) { const path = join(nodeCachedDataRootDir, entry); - deletes.push(promises.stat(path).then(stats => { + deletes.push(Promises.stat(path).then(stats => { // stat check // * only directories // * only when old enough diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts index fc26cb229c3..12055754970 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { join } from 'vs/base/common/path'; -import { readdir, rimraf } from 'vs/base/node/pfs'; +import { Promises, readdir, rimraf } from 'vs/base/node/pfs'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IBackupWorkspacesFormat } from 'vs/platform/backup/node/backup'; @@ -33,7 +32,7 @@ export class StorageDataCleaner extends Disposable { try { // Leverage the backup workspace file to find out which empty workspace is currently in use to // determine which empty workspace storage can safely be deleted - const contents = await promises.readFile(this.backupWorkspacesPath, 'utf8'); + const contents = await Promises.readFile(this.backupWorkspacesPath, 'utf8'); const workspaces = JSON.parse(contents) as IBackupWorkspacesFormat; const emptyWorkspaces = workspaces.emptyWorkspaceInfos.map(info => info.backupFolder); diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index b47c3368c80..8c9be0c67f0 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,7 +5,8 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import { promises, unlinkSync } from 'fs'; +import { unlinkSync } from 'fs'; +import { Promises as FSPromises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { mark } from 'vs/base/common/performance'; @@ -213,7 +214,7 @@ class CodeMain { environmentMainService.globalStorageHome.fsPath, environmentMainService.workspaceStorageHome.fsPath, environmentMainService.backupHome - ].map(path => path ? promises.mkdir(path, { recursive: true }) : undefined)), + ].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)), // Configuration service configurationService.initialize(), diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 83b09f0b3ee..ade2dd62737 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -6,6 +6,7 @@ import { release, hostname } from 'os'; import * as fs from 'fs'; import { gracefulify } from 'graceful-fs'; +import { Promises } from 'vs/base/node/pfs'; import { isAbsolute, join } from 'vs/base/common/path'; import { raceTimeout } from 'vs/base/common/async'; import product from 'vs/platform/product/common/product'; @@ -102,7 +103,7 @@ class CliMain extends Disposable { services.set(INativeEnvironmentService, environmentService); // Init folders - await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined)); + await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); // Log const logLevel = getLogLevel(environmentService); @@ -155,7 +156,7 @@ class CliMain extends Disposable { commonProperties: (async () => { let machineId: string | undefined = undefined; try { - const storageContents = await fs.promises.readFile(join(environmentService.userDataPath, 'storage.json')); + const storageContents = await Promises.readFile(join(environmentService.userDataPath, 'storage.json')); machineId = JSON.parse(storageContents.toString())[machineIdKey]; } catch (error) { if (error.code !== 'ENOENT') { diff --git a/src/vs/platform/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index ac5bb248927..ac9d7f2a86d 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import { createHash } from 'crypto'; import { join } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; -import { writeFileSync, writeFile, readdir, exists, rimraf, RimRafMode } from 'vs/base/node/pfs'; +import { writeFileSync, writeFile, readdir, exists, rimraf, RimRafMode, Promises } from 'vs/base/node/pfs'; import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -49,7 +49,7 @@ export class BackupMainService implements IBackupMainService { async initialize(): Promise { let backups: IBackupWorkspacesFormat; try { - backups = JSON.parse(await fs.promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here + backups = JSON.parse(await Promises.readFile(this.workspacesJsonPath, 'utf8')); // invalid JSON or permission issue can happen here } catch (error) { backups = Object.create(null); } @@ -328,7 +328,7 @@ export class BackupMainService implements IBackupMainService { // Rename backupPath to new empty window backup path const newEmptyWindowBackupPath = this.getBackupPath(newBackupFolder); try { - await fs.promises.rename(backupPath, newEmptyWindowBackupPath); + await Promises.rename(backupPath, newEmptyWindowBackupPath); } catch (error) { this.logService.error(`Backup: Could not rename backup folder: ${error.toString()}`); return false; diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index 9968de89189..a2101b983e9 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -107,7 +107,7 @@ flakySuite('BackupMainService', () => { environmentService = new EnvironmentMainService(parseArgs(process.argv, OPTIONS), { _serviceBrand: undefined, ...product }); - await fs.promises.mkdir(backupHome, { recursive: true }); + await pfs.Promises.mkdir(backupHome, { recursive: true }); configService = new TestConfigurationService(); service = new class TestBackupMainService extends BackupMainService { @@ -446,7 +446,7 @@ flakySuite('BackupMainService', () => { await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -462,7 +462,7 @@ flakySuite('BackupMainService', () => { }; await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -484,7 +484,7 @@ flakySuite('BackupMainService', () => { await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson)); await service.initialize(); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.strictEqual(json.rootURIWorkspaces.length, platform.isLinux ? 3 : 1); if (platform.isLinux) { @@ -500,7 +500,7 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(fooFile); service.registerFolderBackupSync(barFile); assertEqualUris(service.getFolderBackupPaths(), [fooFile, barFile]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [fooFile.toString(), barFile.toString()]); }); @@ -515,7 +515,7 @@ flakySuite('BackupMainService', () => { assert.strictEqual(ws1.workspace.id, service.getWorkspaceBackups()[0].workspace.id); assert.strictEqual(ws2.workspace.id, service.getWorkspaceBackups()[1].workspace.id); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [fooFile.toString(), barFile.toString()]); @@ -528,7 +528,7 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = JSON.parse(buffer); assert.deepStrictEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); }); @@ -538,7 +538,7 @@ flakySuite('BackupMainService', () => { service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); }); @@ -549,12 +549,12 @@ flakySuite('BackupMainService', () => { service.registerFolderBackupSync(barFile); service.unregisterFolderBackupSync(fooFile); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.folderURIWorkspaces, [barFile.toString()]); service.unregisterFolderBackupSync(barFile); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.folderURIWorkspaces, []); }); @@ -566,12 +566,12 @@ flakySuite('BackupMainService', () => { service.registerWorkspaceBackupSync(ws2); service.unregisterWorkspaceBackupSync(ws1.workspace); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); service.unregisterWorkspaceBackupSync(ws2.workspace); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.rootURIWorkspaces, []); }); @@ -581,12 +581,12 @@ flakySuite('BackupMainService', () => { service.registerEmptyWindowBackupSync('bar'); service.unregisterEmptyWindowBackupSync('foo'); - const buffer = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const buffer = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(buffer)); assert.deepStrictEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]); service.unregisterEmptyWindowBackupSync('bar'); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json2 = (JSON.parse(content)); assert.deepStrictEqual(json2.emptyWorkspaceInfos, []); }); @@ -600,7 +600,7 @@ flakySuite('BackupMainService', () => { await service.initialize(); service.unregisterFolderBackupSync(barFile); service.unregisterEmptyWindowBackupSync('test'); - const content = await fs.promises.readFile(backupWorkspacesPath, 'utf-8'); + const content = await pfs.Promises.readFile(backupWorkspacesPath, 'utf-8'); const json = (JSON.parse(content)); assert.deepStrictEqual(json.folderURIWorkspaces, [existingTestFolder1.toString()]); }); @@ -670,8 +670,8 @@ flakySuite('BackupMainService', () => { assert.strictEqual(((await service.getDirtyWorkspaces()).length), 0); try { - await fs.promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); - await fs.promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); + await pfs.Promises.mkdir(path.join(folderBackupPath, Schemas.file), { recursive: true }); + await pfs.Promises.mkdir(path.join(workspaceBackupPath, Schemas.untitled), { recursive: true }); } catch (error) { // ignore - folder might exist already } diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index f118888f897..97fcf5cc46d 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; +import { Promises as FSPromises } from 'vs/base/node/pfs'; import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -77,7 +77,7 @@ export class ExtensionsDownloader extends Disposable { private async rename(from: URI, to: URI, retryUntil: number): Promise { try { - await promises.rename(from.fsPath, to.fsPath); + await FSPromises.rename(from.fsPath, to.fsPath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${from} to ${to} with 'EPERM' error. Trying again...`); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 871c6f93080..3a4d06801a0 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; @@ -140,7 +139,7 @@ export class ExtensionManagementService extends Disposable implements IExtension const collectFilesFromDirectory = async (dir: string): Promise => { let entries = await pfs.readdir(dir); entries = entries.map(e => path.join(dir, e)); - const stats = await Promise.all(entries.map(e => fs.promises.stat(e))); + const stats = await Promise.all(entries.map(e => pfs.Promises.stat(e))); let promise: Promise = Promise.resolve([]); stats.forEach((stat, index) => { const entry = entries[index]; diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index 06de9aea9a2..f489f82362b 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as semver from 'vs/base/common/semver/semver'; import { Disposable } from 'vs/base/common/lifecycle'; import * as pfs from 'vs/base/node/pfs'; @@ -159,7 +158,7 @@ export class ExtensionsScanner extends Disposable { storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined; storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined; const manifestPath = path.join(local.location.fsPath, 'package.json'); - const raw = await fs.promises.readFile(manifestPath, 'utf8'); + const raw = await pfs.Promises.readFile(manifestPath, 'utf8'); const { manifest } = await this.parseManifest(raw); (manifest as ILocalExtensionManifest).__metadata = storedMetadata; await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); @@ -192,7 +191,7 @@ export class ExtensionsScanner extends Disposable { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { - raw = await fs.promises.readFile(this.uninstalledPath, 'utf8'); + raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8'); } catch (err) { if (err.code !== 'ENOENT') { throw err; @@ -251,7 +250,7 @@ export class ExtensionsScanner extends Disposable { private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { try { - await fs.promises.rename(extractPath, renamePath); + await pfs.Promises.rename(extractPath, renamePath); } catch (error) { if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); @@ -393,9 +392,9 @@ export class ExtensionsScanner extends Disposable { private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> { const promises = [ - fs.promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') + pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') .then(raw => this.parseManifest(raw)), - fs.promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') + pfs.Promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') .then(undefined, err => err.code !== 'ENOENT' ? Promise.reject(err) : '{}') .then(raw => JSON.parse(raw)) ]; diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index f4d9fd4fc80..c950ddc09d4 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { open, close, read, write, fdatasync, Stats, promises } from 'fs'; -import { promisify } from 'util'; +import { Stats } from 'fs'; import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability, isFileOpenForWriteOptions } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent } from 'vs/base/node/pfs'; +import { SymlinkSupport, move, copy, rimraf, RimRafMode, exists, readdir, IDirent, Promises } from 'vs/base/node/pfs'; import { normalize, basename, dirname } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; @@ -152,7 +151,7 @@ export class DiskFileSystemProvider extends Disposable implements try { const filePath = this.toFilePath(resource); - return await promises.readFile(filePath); + return await Promises.readFile(filePath); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -216,7 +215,7 @@ export class DiskFileSystemProvider extends Disposable implements try { const { stat } = await SymlinkSupport.stat(filePath); if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) { - await promises.chmod(filePath, stat.mode | 0o200); + await Promises.chmod(filePath, stat.mode | 0o200); } } catch (error) { this.logService.trace(error); // ignore any errors here and try to just write @@ -232,7 +231,7 @@ export class DiskFileSystemProvider extends Disposable implements // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows // (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams // (see https://github.com/microsoft/vscode/issues/6363) - await promises.truncate(filePath, 0); + await Promises.truncate(filePath, 0); // After a successful truncate() the flag can be set to 'r+' which will not truncate. flags = 'r+'; @@ -256,7 +255,7 @@ export class DiskFileSystemProvider extends Disposable implements flags = 'r'; } - const handle = await promisify(open)(filePath, flags); + const handle = await Promises.open(filePath, flags); // remember this handle to track file position of the handle // we init the position to 0 since the file descriptor was @@ -290,7 +289,7 @@ export class DiskFileSystemProvider extends Disposable implements // to flush the contents to disk if possible. if (this.writeHandles.delete(fd) && this.canFlush) { try { - await promisify(fdatasync)(fd); + await Promises.fdatasync(fd); } catch (error) { // In some exotic setups it is well possible that node fails to sync // In that case we disable flushing and log the error to our logger @@ -299,7 +298,7 @@ export class DiskFileSystemProvider extends Disposable implements } } - return await promisify(close)(fd); + return await Promises.close(fd); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -310,7 +309,7 @@ export class DiskFileSystemProvider extends Disposable implements let bytesRead: number | null = null; try { - const result = await promisify(read)(fd, data, offset, length, normalizedPos); + const result = await Promises.read(fd, data, offset, length, normalizedPos); if (typeof result === 'number') { bytesRead = result; // node.d.ts fail @@ -396,7 +395,7 @@ export class DiskFileSystemProvider extends Disposable implements let bytesWritten: number | null = null; try { - const result = await promisify(write)(fd, data, offset, length, normalizedPos); + const result = await Promises.write(fd, data, offset, length, normalizedPos); if (typeof result === 'number') { bytesWritten = result; // node.d.ts fail @@ -418,7 +417,7 @@ export class DiskFileSystemProvider extends Disposable implements async mkdir(resource: URI): Promise { try { - await promises.mkdir(this.toFilePath(resource)); + await Promises.mkdir(this.toFilePath(resource)); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -438,7 +437,7 @@ export class DiskFileSystemProvider extends Disposable implements if (opts.recursive) { await rimraf(filePath, RimRafMode.MOVE); } else { - await promises.unlink(filePath); + await Promises.unlink(filePath); } } diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 7048ce0225b..dc170c38139 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -10,9 +10,9 @@ import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { flakySuite, getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils'; import { join, basename, dirname, posix } from 'vs/base/common/path'; -import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs'; +import { copy, Promises, rimraf, rimrafSync } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; -import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream, promises } from 'fs'; +import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream } from 'fs'; import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions, FilePermission } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; import { isLinux, isWindows } from 'vs/base/common/platform'; @@ -417,7 +417,7 @@ flakySuite('Disk File Service', function () { test('resolve - folder symbolic link', async () => { const link = URI.file(join(testDir, 'deep-link')); - await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); + await Promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction'); const resolved = await service.resolve(link); assert.strictEqual(resolved.children!.length, 4); @@ -427,7 +427,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => { const link = URI.file(join(testDir, 'lorem.txt-linked')); - await promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); + await Promises.symlink(join(testDir, 'lorem.txt'), link.fsPath); const resolved = await service.resolve(link); assert.strictEqual(resolved.isDirectory, false); @@ -435,7 +435,7 @@ flakySuite('Disk File Service', function () { }); test('resolve - symbolic link pointing to non-existing file does not break', async () => { - await promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); + await Promises.symlink(join(testDir, 'foo'), join(testDir, 'bar'), 'junction'); const resolved = await service.resolve(URI.file(testDir)); assert.strictEqual(resolved.isDirectory, true); @@ -486,7 +486,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (exists)', async () => { const target = URI.file(join(testDir, 'lorem.txt')); const link = URI.file(join(testDir, 'lorem.txt-linked')); - await promises.symlink(target.fsPath, link.fsPath); + await Promises.symlink(target.fsPath, link.fsPath); const source = await service.resolve(link); @@ -508,7 +508,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('deleteFile - symbolic link (pointing to non-existing file)', async () => { const target = URI.file(join(testDir, 'foo')); const link = URI.file(join(testDir, 'bar')); - await promises.symlink(target.fsPath, link.fsPath); + await Promises.symlink(target.fsPath, link.fsPath); let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); @@ -1601,7 +1601,7 @@ flakySuite('Disk File Service', function () { (isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => { const link = URI.file(join(testDir, 'small.js-link')); - await promises.symlink(join(testDir, 'small.js'), link.fsPath); + await Promises.symlink(join(testDir, 'small.js'), link.fsPath); let error: FileOperationError | undefined = undefined; try { @@ -1937,8 +1937,8 @@ flakySuite('Disk File Service', function () { await service.writeFile(lockedFile, VSBuffer.fromString('Locked File')); - const stats = await promises.stat(lockedFile.fsPath); - await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); + const stats = await Promises.stat(lockedFile.fsPath); + await Promises.chmod(lockedFile.fsPath, stats.mode & ~0o200); let error; const newContent = 'Updates to locked file'; @@ -2100,7 +2100,7 @@ flakySuite('Disk File Service', function () { (runWatchTests && !isWindows /* windows: cannot create file symbolic link without elevated context */ ? test : test.skip)('watch - file symbolic link', async () => { const toWatch = URI.file(join(testDir, 'lorem.txt-linked')); - await promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); + await Promises.symlink(join(testDir, 'lorem.txt'), toWatch.fsPath); const promise = assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]]); setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50); @@ -2228,7 +2228,7 @@ flakySuite('Disk File Service', function () { (runWatchTests ? test : test.skip)('watch - folder (non recursive) - symbolic link - change file', async () => { const watchDir = URI.file(join(testDir, 'deep-link')); - await promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); + await Promises.symlink(join(testDir, 'deep'), watchDir.fsPath, 'junction'); const file = URI.file(join(watchDir.fsPath, 'index.html')); writeFileSync(file.fsPath, 'Init'); diff --git a/src/vs/platform/localizations/node/localizations.ts b/src/vs/platform/localizations/node/localizations.ts index 02f3df5a911..e1ef23c7b36 100644 --- a/src/vs/platform/localizations/node/localizations.ts +++ b/src/vs/platform/localizations/node/localizations.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { writeFile } from 'vs/base/node/pfs'; -import { promises } from 'fs'; +import { Promises, writeFile } from 'vs/base/node/pfs'; import { createHash } from 'crypto'; import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -158,7 +157,7 @@ class LanguagePacksCache extends Disposable { private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise { return this.languagePacksFileLimiter.queue(() => { let result: T | null = null; - return promises.readFile(this.languagePacksFilePath, 'utf8') + return Promises.readFile(this.languagePacksFilePath, 'utf8') .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) .then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) .then(languagePacks => { result = fn(languagePacks); return languagePacks; }) diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index b75c64d506c..4d4317e9d65 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; import { localize } from 'vs/nls'; @@ -20,7 +19,7 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; -import { exists, SymlinkSupport } from 'vs/base/node/pfs'; +import { exists, Promises, SymlinkSupport } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -277,7 +276,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } // Different source, delete it first - await fs.promises.unlink(source); + await Promises.unlink(source); } catch (error) { if (error.code !== 'ENOENT') { throw error; // throw on any error but file not found @@ -285,7 +284,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } try { - await fs.promises.symlink(target, source); + await Promises.symlink(target, source); } catch (error) { if (error.code !== 'EACCES' && error.code !== 'ENOENT') { throw error; @@ -313,7 +312,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const { source } = await this.getShellCommandLink(); try { - await fs.promises.unlink(source); + await Promises.unlink(source); } catch (error) { switch (error.code) { case 'EACCES': diff --git a/src/vs/platform/state/test/electron-main/state.test.ts b/src/vs/platform/state/test/electron-main/state.test.ts index 568aa978a5a..40b751c6c1e 100644 --- a/src/vs/platform/state/test/electron-main/state.test.ts +++ b/src/vs/platform/state/test/electron-main/state.test.ts @@ -5,11 +5,11 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; -import { promises, readFileSync } from 'fs'; +import { readFileSync } from 'fs'; import { join } from 'vs/base/common/path'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { FileStorage } from 'vs/platform/state/electron-main/stateMainService'; -import { rimraf, writeFileSync } from 'vs/base/node/pfs'; +import { Promises, rimraf, writeFileSync } from 'vs/base/node/pfs'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -33,7 +33,7 @@ flakySuite('StateMainService', () => { diskFileSystemProvider = new DiskFileSystemProvider(logService); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - return promises.mkdir(testDir, { recursive: true }); + return Promises.mkdir(testDir, { recursive: true }); }); teardown(() => { diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index 3cd5051587f..16ca034a873 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { promises } from 'fs'; -import { exists, writeFile } from 'vs/base/node/pfs'; +import { exists, Promises, writeFile } from 'vs/base/node/pfs'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; @@ -276,7 +275,7 @@ export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMai } // Ensure storage folder exists - await promises.mkdir(workspaceStorageFolderPath, { recursive: true }); + await Promises.mkdir(workspaceStorageFolderPath, { recursive: true }); // Write metadata into folder (but do not await) this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index df0737399af..b37b6ab2c15 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -18,6 +18,7 @@ import { localize } from 'vs/nls'; import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper'; import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { timeout } from 'vs/base/common/async'; +import { Promises } from 'vs/base/node/pfs'; // Writing large amounts of data can be corrupted for some reason, after looking into this is // appears to be a race condition around writing to the FD which may be based on how powerful the @@ -182,7 +183,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private async _validateCwd(): Promise { try { - const result = await fs.promises.stat(this._initialCwd); + const result = await Promises.stat(this._initialCwd); if (!result.isDirectory()) { return { message: localize('launchFail.cwdNotDirectory', "Starting directory (cwd) \"{0}\" is not a directory", this._initialCwd.toString()) }; } @@ -200,7 +201,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess throw new Error('IShellLaunchConfig.executable not set'); } try { - const result = await fs.promises.stat(slc.executable); + const result = await Promises.stat(slc.executable); if (!result.isFile() && !result.isSymbolicLink()) { return { message: localize('launchFail.executableIsNotFileOrSymlink', "Path to shell executable \"{0}\" is not a file of a symlink", slc.executable) }; } diff --git a/src/vs/platform/terminal/node/terminalProfiles.ts b/src/vs/platform/terminal/node/terminalProfiles.ts index f2777cd794b..7250ad21837 100644 --- a/src/vs/platform/terminal/node/terminalProfiles.ts +++ b/src/vs/platform/terminal/node/terminalProfiles.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import { normalize, basename, delimiter } from 'vs/base/common/path'; import { enumeratePowerShellInstallations } from 'vs/base/node/powershell'; import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; @@ -28,7 +27,7 @@ export function detectAvailableProfiles( ): Promise { fsProvider = fsProvider || { existsFile: pfs.SymlinkSupport.existsFile, - readFile: fs.promises.readFile + readFile: pfs.Promises.readFile }; if (isWindows) { return detectAvailableWindowsProfiles( @@ -273,7 +272,7 @@ async function detectAvailableUnixProfiles( // Add non-quick launch profiles if (includeDetectedProfiles) { - const contents = await fsProvider.readFile('/etc/shells', 'utf8'); + const contents = (await fsProvider.readFile('/etc/shells')).toString(); const profiles = testPaths || contents.split('\n').filter(e => e.trim().indexOf('#') !== 0 && e.trim().length > 0); const counts: Map = new Map(); for (const profile of profiles) { @@ -338,7 +337,7 @@ async function validateProfilePaths(profileName: string, defaultProfileName: str export interface IFsProvider { existsFile(path: string): Promise, - readFile(path: string, options: { encoding: BufferEncoding, flag?: string | number } | BufferEncoding): Promise; + readFile(path: string): Promise; } export interface IProfileVariableResolver { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index eab8c3f1807..93424cad965 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -54,7 +54,7 @@ export class Win32UpdateService extends AbstractUpdateService { @memoize get cachePath(): Promise { const result = path.join(tmpdir(), `vscode-update-${this.productService.target}-${process.arch}`); - return fs.promises.mkdir(result, { recursive: true }).then(() => result); + return pfs.Promises.mkdir(result, { recursive: true }).then(() => result); } constructor( @@ -145,7 +145,7 @@ export class Win32UpdateService extends AbstractUpdateService { return this.requestService.request({ url }, CancellationToken.None) .then(context => this.fileService.writeFile(URI.file(downloadPath), context.stream)) .then(hash ? () => checksum(downloadPath, update.hash) : () => undefined) - .then(() => fs.promises.rename(downloadPath, updatePackagePath)) + .then(() => pfs.Promises.rename(downloadPath, updatePackagePath)) .then(() => updatePackagePath); }); }).then(packagePath => { @@ -195,7 +195,7 @@ export class Win32UpdateService extends AbstractUpdateService { const promises = versions.filter(filter).map(async one => { try { - await fs.promises.unlink(path.join(cachePath, one)); + await pfs.Promises.unlink(path.join(cachePath, one)); } catch (err) { // ignore } diff --git a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts index 8edbdc00143..47a5be15c90 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesManagementMainService.ts @@ -6,8 +6,8 @@ import { toWorkspaceFolders, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace, isWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { join, dirname } from 'vs/base/common/path'; -import { writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; -import { promises, readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; +import { writeFile, rimrafSync, readdirSync, writeFileSync, Promises } from 'vs/base/node/pfs'; +import { readFileSync, existsSync, mkdirSync, statSync, Stats } from 'fs'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { Event, Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; @@ -142,7 +142,7 @@ export class WorkspacesManagementMainService extends Disposable implements IWork const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); const configPath = workspace.configPath.fsPath; - await promises.mkdir(dirname(configPath), { recursive: true }); + await Promises.mkdir(dirname(configPath), { recursive: true }); await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); return workspace; diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts index 1b524310e9c..3769d7324bb 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesManagementMainService.test.ts @@ -109,7 +109,7 @@ flakySuite('WorkspacesManagementMainService', () => { service = new WorkspacesManagementMainService(environmentMainService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService(), productService); - return fs.promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); + return pfs.Promises.mkdir(untitledWorkspacesHomePath, { recursive: true }); }); teardown(() => { diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 78a955a7330..9935633b766 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -8,8 +8,7 @@ import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { toLocalISOString } from 'vs/base/common/date'; -import { SymlinkSupport } from 'vs/base/node/pfs'; -import { promises } from 'fs'; +import { Promises, SymlinkSupport } from 'vs/base/node/pfs'; import { AbstractExtHostOutputChannel, ExtHostPushOutputChannel, ExtHostOutputService, LazyOutputChannel } from 'vs/workbench/api/common/extHostOutput'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -111,7 +110,7 @@ export class ExtHostOutputService2 extends ExtHostOutputService { const outputDirPath = join(this._logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`); const exists = await SymlinkSupport.existsDirectory(outputDirPath); if (!exists) { - await promises.mkdir(outputDirPath, { recursive: true }); + await Promises.mkdir(outputDirPath, { recursive: true }); } const fileName = `${this._namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}`; const file = URI.file(join(outputDirPath, `${fileName}.log`)); diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index a33f7851f34..caefe078c1a 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -11,7 +11,6 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { URI } from 'vs/base/common/uri'; import { exec } from 'child_process'; import * as resources from 'vs/base/common/resources'; -import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { isLinux } from 'vs/base/common/platform'; @@ -365,8 +364,8 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe let tcp: string = ''; let tcp6: string = ''; try { - tcp = await fs.promises.readFile('/proc/net/tcp', 'utf8'); - tcp6 = await fs.promises.readFile('/proc/net/tcp6', 'utf8'); + tcp = await pfs.Promises.readFile('/proc/net/tcp', 'utf8'); + tcp6 = await pfs.Promises.readFile('/proc/net/tcp6', 'utf8'); } catch (e) { // File reading error. No additional handling needed. } @@ -387,10 +386,10 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe try { const pid: number = Number(childName); const childUri = resources.joinPath(URI.file('/proc'), childName); - const childStat = await fs.promises.stat(childUri.fsPath); + const childStat = await pfs.Promises.stat(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { - const cwd = await fs.promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = await fs.promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const cwd = await pfs.Promises.readlink(resources.joinPath(childUri, 'cwd').fsPath); + const cmd = await pfs.Promises.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); processes.push({ pid, cwd, cmd }); } } catch (e) { diff --git a/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts b/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts index c282f94cbf0..8e1eb26e79e 100644 --- a/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts +++ b/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts @@ -226,11 +226,11 @@ suite('Workbench - TerminalProfiles', () => { async existsFile(path: string): Promise { return expectedPaths.includes(path); }, - async readFile(path: string, options: { encoding: BufferEncoding, flag?: string | number } | BufferEncoding): Promise { + async readFile(path: string): Promise { if (path !== '/etc/shells') { fail('Unexected path'); } - return etcShellsContent; + return Buffer.from(etcShellsContent); } }; return provider; diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index c441c61c1f6..aa78078e14e 100644 --- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as errors from 'vs/base/common/errors'; @@ -152,7 +151,7 @@ export class CachedExtensionScanner { const cacheFile = path.join(cacheFolder, cacheKey); try { - const cacheRawContents = await fs.promises.readFile(cacheFile, 'utf8'); + const cacheRawContents = await pfs.Promises.readFile(cacheFile, 'utf8'); return JSON.parse(cacheRawContents); } catch (err) { // That's ok... @@ -166,7 +165,7 @@ export class CachedExtensionScanner { const cacheFile = path.join(cacheFolder, cacheKey); try { - await fs.promises.mkdir(cacheFolder, { recursive: true }); + await pfs.Promises.mkdir(cacheFolder, { recursive: true }); } catch (err) { // That's ok... } @@ -185,7 +184,7 @@ export class CachedExtensionScanner { } try { - const folderStat = await fs.promises.stat(input.absoluteFolderPath); + const folderStat = await pfs.Promises.stat(input.absoluteFolderPath); input.mtime = folderStat.mtime.getTime(); } catch (err) { // That's ok... @@ -225,7 +224,7 @@ export class CachedExtensionScanner { private static async _readTranslationConfig(): Promise { if (platform.translationsConfigFile) { try { - const content = await fs.promises.readFile(platform.translationsConfigFile, 'utf8'); + const content = await pfs.Promises.readFile(platform.translationsConfigFile, 'utf8'); return JSON.parse(content) as Translations; } catch (err) { // no problemo @@ -265,7 +264,7 @@ export class CachedExtensionScanner { const builtInExtensions = Promise.resolve(productService.builtInExtensions || []); const controlFilePath = joinPath(environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json').fsPath; - const controlFile = fs.promises.readFile(controlFilePath, 'utf8') + const controlFile = pfs.Promises.readFile(controlFilePath, 'utf8') .then(raw => JSON.parse(raw), () => ({} as any)); const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index 076ad50b84c..14106fd427a 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as semver from 'vs/base/common/semver/semver'; @@ -60,7 +59,7 @@ class ExtensionManifestParser extends ExtensionManifestHandler { } public parse(): Promise { - return fs.promises.readFile(this._absoluteManifestPath).then((manifestContents) => { + return pfs.Promises.readFile(this._absoluteManifestPath).then((manifestContents) => { const errors: json.ParseError[] = []; const manifest = ExtensionManifestParser._fastParseJSON(manifestContents.toString(), errors); if (json.getNodeType(manifest) !== 'object') { @@ -130,7 +129,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { let translationPath = this._nlsConfig.translations[translationId]; let localizedMessages: Promise; if (translationPath) { - localizedMessages = fs.promises.readFile(translationPath, 'utf8').then((content) => { + localizedMessages = pfs.Promises.readFile(translationPath, 'utf8').then((content) => { let errors: json.ParseError[] = []; let translationBundle: TranslationBundle = json.parse(content, errors); if (errors.length > 0) { @@ -155,7 +154,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { if (!messageBundle.localized) { return { values: undefined, default: messageBundle.original }; } - return fs.promises.readFile(messageBundle.localized, 'utf8').then(messageBundleContent => { + return pfs.Promises.readFile(messageBundle.localized, 'utf8').then(messageBundleContent => { let errors: json.ParseError[] = []; let messages: MessageBag = json.parse(messageBundleContent, errors); if (errors.length > 0) { @@ -204,7 +203,7 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { private static resolveOriginalMessageBundle(originalMessageBundle: string | null, errors: json.ParseError[]) { return new Promise<{ [key: string]: string; } | null>((c, e) => { if (originalMessageBundle) { - fs.promises.readFile(originalMessageBundle).then(originalBundleContent => { + pfs.Promises.readFile(originalMessageBundle).then(originalBundleContent => { c(json.parse(originalBundleContent.toString(), errors)); }, (err) => { c(null); @@ -553,7 +552,7 @@ export class ExtensionScanner { let obsolete: { [folderName: string]: boolean; } = {}; if (!isBuiltin) { try { - const obsoleteFileContents = await fs.promises.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8'); + const obsoleteFileContents = await pfs.Promises.readFile(path.join(absoluteFolderPath, '.obsolete'), 'utf8'); obsolete = JSON.parse(obsoleteFileContents); } catch (err) { // Don't care diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts index 64967e8838c..d17478838fe 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts @@ -5,11 +5,10 @@ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { promises } from 'fs'; import { getPathFromAmdModule } from 'vs/base/test/node/testUtils'; import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { ScanCodeBinding } from 'vs/base/common/scanCode'; -import { writeFile } from 'vs/base/node/pfs'; +import { Promises, writeFile } from 'vs/base/node/pfs'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper'; @@ -53,7 +52,7 @@ export function assertResolveUserBinding(mapper: IKeyboardMapper, parts: (Simple } export function readRawMapping(file: string): Promise { - return promises.readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { + return Promises.readFile(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}.js`)).then((buff) => { let contents = buff.toString(); let func = new Function('define', contents); let rawMappings: T | null = null; @@ -67,7 +66,7 @@ export function readRawMapping(file: string): Promise { export function assertMapping(writeFileIfDifferent: boolean, mapper: IKeyboardMapper, file: string): Promise { const filePath = path.normalize(getPathFromAmdModule(require, `vs/workbench/services/keybinding/test/electron-browser/${file}`)); - return promises.readFile(filePath).then((buff) => { + return Promises.readFile(filePath).then((buff) => { const expected = buff.toString().replace(/\r\n/g, '\n'); const actual = mapper.dumpDebugInfo().replace(/\r\n/g, '\n'); if (actual !== expected && writeFileIfDifferent) { diff --git a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts index b311ad724f6..08e0aeee528 100644 --- a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts @@ -10,7 +10,7 @@ import { release, tmpdir, hostname } from 'os'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { IStorageService, StorageScope, InMemoryStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; -import { rimraf } from 'vs/base/node/pfs'; +import { Promises, rimraf } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -44,7 +44,7 @@ suite('Telemetry - common properties', function () { }); test('default', async function () { - await fs.promises.mkdir(parentDir, { recursive: true }); + await Promises.mkdir(parentDir, { recursive: true }); fs.writeFileSync(installSource, 'my.install.source'); const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); assert.ok('commitHash' in props); diff --git a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts index 57f55b51b9d..e46be38a1ee 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { tmpdir } from 'os'; -import { promises } from 'fs'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { Schemas } from 'vs/base/common/network'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { rimraf, copy, exists } from 'vs/base/node/pfs'; +import { rimraf, copy, exists, Promises } from 'vs/base/node/pfs'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -32,7 +31,7 @@ flakySuite('Files - NativeTextFileService i/o', function () { function readFile(path: string): Promise; function readFile(path: string, encoding: BufferEncoding): Promise; function readFile(path: string, encoding?: BufferEncoding): Promise { - return promises.readFile(path, encoding); + return Promises.readFile(path, encoding); } createSuite({ @@ -70,7 +69,7 @@ flakySuite('Files - NativeTextFileService i/o', function () { }, exists, - stat: promises.stat, + stat: Promises.stat, readFile, detectEncodingByBOM }); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts index b3e9213dfaa..534c8927890 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts @@ -10,9 +10,9 @@ import { createHash } from 'crypto'; import { insert } from 'vs/base/common/arrays'; import { hash } from 'vs/base/common/hash'; import { isEqual } from 'vs/base/common/resources'; -import { promises, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { dirname, join } from 'vs/base/common/path'; -import { readdirSync, rimraf, writeFile } from 'vs/base/node/pfs'; +import { Promises, readdirSync, rimraf, writeFile } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { WorkingCopyBackupsModel, hashIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; @@ -149,7 +149,7 @@ suite('WorkingCopyBackupService', () => { service = new NodeTestWorkingCopyBackupService(testDir, workspaceBackupPath); - await promises.mkdir(backupHome, { recursive: true }); + await Promises.mkdir(backupHome, { recursive: true }); return writeFile(workspacesJsonPath, ''); }); @@ -990,7 +990,7 @@ suite('WorkingCopyBackupService', () => { const sourceDir = getPathFromAmdModule(require, './fixtures'); - const buffer = await promises.readFile(join(sourceDir, 'binary.txt')); + const buffer = await Promises.readFile(join(sourceDir, 'binary.txt')); const hash = createHash('md5').update(buffer).digest('base64'); await service.backup(identifier, bufferToReadable(VSBuffer.wrap(buffer)), undefined, { binaryTest: 'true' }); @@ -1067,7 +1067,7 @@ suite('WorkingCopyBackupService', () => { test('create', async () => { const fooBackupPath = join(workspaceBackupPath, fooFile.scheme, hashIdentifier(toUntypedWorkingCopyId(fooFile))); - await promises.mkdir(dirname(fooBackupPath), { recursive: true }); + await Promises.mkdir(dirname(fooBackupPath), { recursive: true }); writeFileSync(fooBackupPath, 'foo'); const model = await WorkingCopyBackupsModel.create(URI.file(workspaceBackupPath), service.fileService); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts index 20ae4e5902e..0cbfa6c9876 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts @@ -6,9 +6,8 @@ import * as assert from 'assert'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { tmpdir } from 'os'; -import { promises } from 'fs'; import { join } from 'vs/base/common/path'; -import { rimraf, writeFile } from 'vs/base/node/pfs'; +import { Promises, rimraf, writeFile } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; import { hash } from 'vs/base/common/hash'; @@ -105,8 +104,8 @@ flakySuite('WorkingCopyBackupTracker (native)', function () { disposables.add(registerTestFileEditor()); - await promises.mkdir(backupHome, { recursive: true }); - await promises.mkdir(workspaceBackupPath, { recursive: true }); + await Promises.mkdir(backupHome, { recursive: true }); + await Promises.mkdir(workspaceBackupPath, { recursive: true }); return writeFile(workspacesJsonPath, ''); }); diff --git a/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts b/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts index b0c5af9bbdb..6f04faa6df3 100644 --- a/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/test/electron-browser/colorRegistry.releaseTest.ts @@ -6,7 +6,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IColorRegistry, Extensions, ColorContribution } from 'vs/platform/theme/common/colorRegistry'; import { asText } from 'vs/platform/request/common/request'; -import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; import * as path from 'vs/base/common/path'; import * as assert from 'assert'; @@ -106,7 +105,7 @@ async function getColorsFromExtension(): Promise<{ [id: string]: string }> { let result: { [id: string]: string } = Object.create(null); for (let folder of extFolders) { try { - let packageJSON = JSON.parse((await fs.promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); + let packageJSON = JSON.parse((await pfs.Promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); let contributes = packageJSON['contributes']; if (contributes) { let colors = contributes['colors']; From 100a70731dbf4b811744b5153e45ebc11c37b93d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 12:13:04 +0200 Subject: [PATCH 249/270] SVG images are not render in the tooltip (fix #123688) --- src/vs/platform/protocol/electron-main/protocolMainService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/protocol/electron-main/protocolMainService.ts b/src/vs/platform/protocol/electron-main/protocolMainService.ts index bd1eec7f933..798aa745d4d 100644 --- a/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -22,7 +22,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ declare readonly _serviceBrand: undefined; private readonly validRoots = TernarySearchTree.forUris(() => !isLinux); - private readonly validExtensions = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384 + private readonly validExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384 constructor( @INativeEnvironmentService environmentService: INativeEnvironmentService, From 996dfffd637b89fb63f0a026286dcfe2fe1590e0 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 21 May 2021 12:57:41 +0200 Subject: [PATCH 250/270] enforce proposed API for shortTitle proposal, https://github.com/microsoft/vscode/issues/124355 --- src/vs/workbench/api/common/menusExtensionPoint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 1b4dbf22065..71aa38a9fee 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -596,7 +596,7 @@ commandsExtensionPoint.setHandler(extensions => { bucket.push({ id: command, title, - shortTitle, + shortTitle: extension.description.enableProposedApi ? shortTitle : undefined, category, precondition: ContextKeyExpr.deserialize(enablement), icon: absoluteIcon From b9f20119393077c0dc39aef3aaa968b2af18c867 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 21 May 2021 09:13:36 -0400 Subject: [PATCH 251/270] Bump distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e03293fb96e..4428a3c4a7b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.57.0", - "distro": "ecdb89a80900940251ce4a8083f6b2e4b032b583", + "distro": "8af067d7af4cb9a7acf8efcdfb255c1e68452728", "author": { "name": "Microsoft Corporation" }, From bbe3b2266b56da8db1b67be07f0ebb04c091d8af Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 21 May 2021 15:36:18 +0200 Subject: [PATCH 252/270] allow to signal limited functionality in virtual workspaces --- src/vs/base/common/product.ts | 7 +++++- .../platform/extensions/common/extensions.ts | 25 +++++++++++++++---- .../extensions/browser/extensionsActions.ts | 22 ++++++++++------ .../extensions/browser/extensionsViews.ts | 2 +- .../workspace/browser/workspaceTrustEditor.ts | 6 ++--- .../browser/extensionEnablementService.ts | 4 +-- .../extensionManifestPropertiesService.ts | 11 ++++++-- .../extensions/common/extensionsRegistry.ts | 24 ++++++++++++++++-- 8 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index fb1afd73cef..3b53fd808df 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -25,6 +25,11 @@ export type ExtensionUntrustedWorkspaceSupport = { readonly override?: boolean | 'limited' }; +export type ExtensionVirtualWorkspaceSupport = { + readonly default?: boolean, + readonly override?: boolean +}; + export interface IProductConfiguration { readonly version: string; readonly date?: string; @@ -122,7 +127,7 @@ export interface IProductConfiguration { readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[]; }; readonly extensionAllowedProposedApi?: readonly string[]; readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport }; - readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: { default?: boolean, override?: boolean } }; + readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport }; readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 3fec343e5eb..b603722ec4a 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -166,15 +166,30 @@ export interface IExtensionContributions { } export interface IExtensionCapabilities { - readonly virtualWorkspaces?: boolean; + readonly virtualWorkspaces?: ExtensionVirtualWorkpaceSupport; readonly untrustedWorkspaces?: ExtensionUntrustedWorkspaceSupport; } -export type ExtensionKind = 'ui' | 'workspace' | 'web'; -export type ExtensionUntrustedWorkpaceSupportType = boolean | 'limited'; -export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: 'limited', description: string, restrictedConfigurations?: string[] }; -export type ExtensionVirtualWorkpaceSupportType = boolean | 'partial'; + +export type ExtensionKind = 'ui' | 'workspace' | 'web'; + +export type LimitedWorkpaceSupportType = 'limited'; +export type ExtensionUntrustedWorkpaceSupportType = boolean | LimitedWorkpaceSupportType; +export type ExtensionUntrustedWorkspaceSupport = { supported: true; } | { supported: false, description: string } | { supported: LimitedWorkpaceSupportType, description: string, restrictedConfigurations?: string[] }; + +export type ExtensionVirtualWorkpaceSupportType = boolean | LimitedWorkpaceSupportType; +export type ExtensionVirtualWorkpaceSupport = boolean | { supported: true; } | { supported: false | LimitedWorkpaceSupportType, description: string }; + +export function getWorkpaceSupportTypeMessage(supportType: ExtensionUntrustedWorkspaceSupport | ExtensionVirtualWorkpaceSupport | undefined): string | undefined { + if (typeof supportType === 'object' && supportType !== null) { + if (supportType.supported !== true) { + return supportType.description; + } + } + return undefined; +} + export function isIExtensionIdentifier(thing: any): thing is IExtensionIdentifier { return thing diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index ca8a3e6f58d..6e4fea0a6a8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -18,7 +18,7 @@ import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, I import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, getWorkpaceSupportTypeMessage } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IFileService, IFileContent } from 'vs/platform/files/common/files'; @@ -62,6 +62,7 @@ import { isWeb } from 'vs/base/common/platform'; import { isWorkspaceTrustEnabled } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; function getRelativeDateLabel(date: Date): string { const delta = new Date().getTime() - date.getTime(); @@ -2092,6 +2093,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { @IExtensionService private readonly extensionService: IExtensionService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, ) { super('extensions.install', '', `${SystemDisabledWarningAction.CLASS} hide`, false); this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); @@ -2117,10 +2119,17 @@ export class SystemDisabledWarningAction extends ExtensionAction { ) { return; } - if (this.extension.enablementState === EnablementState.DisabledByVirtualWorkspace) { - this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; - this.tooltip = localize('disabled because of virtual workspace', "This extension has been disabled because it does not support virtual workspaces."); - return; + + if (isVirtualWorkspace(this.contextService.getWorkspace())) { + const virtualSupportType = this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(this.extension.local.manifest); + if (virtualSupportType !== true) { + this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; + const details = getWorkpaceSupportTypeMessage(this.extension.local.manifest.capabilities?.virtualWorkspaces); + this.tooltip = details || (virtualSupportType === 'limited' ? + localize('extension limited because of virtual workspace', "This extension has limited features because the current workspace is virtual.") : + localize('disabled because of virtual workspace', "This extension has been disabled because it does not support virtual workspaces.")); + return; + } } if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { if (isLanguagePackExtension(this.extension.local.manifest)) { @@ -2166,8 +2175,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { const untrustedSupportType = this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(this.extension.local.manifest); if (isWorkspaceTrustEnabled(this.configurationService) && untrustedSupportType !== true && !this.workspaceTrustService.isWorkpaceTrusted()) { - const untrustedWorkspaceSupport = this.extension.local.manifest.capabilities?.untrustedWorkspaces; - const untrustedDetails = untrustedWorkspaceSupport?.supported !== true ? untrustedWorkspaceSupport?.description : undefined; + const untrustedDetails = getWorkpaceSupportTypeMessage(this.extension.local.manifest.capabilities?.untrustedWorkspaces); this.enabled = true; this.class = `${SystemDisabledWarningAction.TRUST_CLASS}`; this.tooltip = untrustedDetails || (untrustedSupportType === 'limited' ? diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 87f38eeb22a..79110057fb3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -581,7 +581,7 @@ export class ExtensionsListView extends ViewPane { if (type === 'virtual') { // show limited and disabled extensions unless disabled because of a untrusted workspace - local = local.filter(extension => inVirtualWorkspace && hasVirtualSupportType(extension, partial ? 'partial' : false) && !(inRestrictedWorkspace && hasRestrictedSupportType(extension, false))); + local = local.filter(extension => inVirtualWorkspace && hasVirtualSupportType(extension, partial ? 'limited' : false) && !(inRestrictedWorkspace && hasRestrictedSupportType(extension, false))); } else if (type === 'untrusted') { // show limited and disabled extensions unless disabled because of a virtual workspace local = local.filter(extension => inRestrictedWorkspace && hasRestrictedSupportType(extension, partial ? 'limited' : false) && !(inVirtualWorkspace && hasVirtualSupportType(extension, false))); diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index cf2f44347f4..92049a21903 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -33,7 +33,7 @@ import { IPromptChoiceWithMenu } from 'vs/platform/notification/common/notificat import { Link } from 'vs/platform/opener/browser/link'; import product from 'vs/platform/product/common/product'; import { Registry } from 'vs/platform/registry/common/platform'; -import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { buttonBackground, buttonSecondaryBackground, editorErrorForeground } from 'vs/platform/theme/common/colorRegistry'; @@ -742,9 +742,9 @@ export class WorkspaceTrustEditor extends EditorPane { private getExtensionCountByUntrustedWorkspaceSupport(extensions: IExtensionStatus[], trustRequestType: ExtensionUntrustedWorkpaceSupportType): number { const filtered = extensions.filter(ext => this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(ext.local.manifest) === trustRequestType); const set = new Set(); + const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace()); for (const ext of filtered) { - const isVirtualWorkspace = getVirtualWorkspaceScheme(this.workspaceService.getWorkspace()) !== undefined; - if (!isVirtualWorkspace || this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(ext.local.manifest) !== false) { + if (!inVirtualWorkspace || this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(ext.local.manifest) !== false) { set.add(ext.identifier.id); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 4b6e684aeb5..46a96306cb0 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -26,7 +26,7 @@ import { IExtensionBisectService } from 'vs/workbench/services/extensionManageme import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { Promises } from 'vs/base/common/async'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; +import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -229,7 +229,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private _isDisabledByVirtualWorkspace(extension: IExtension): boolean { - if (getVirtualWorkspaceScheme(this.contextService.getWorkspace()) !== undefined) { + if (isVirtualWorkspace(this.contextService.getWorkspace())) { return this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.manifest) === false; } return false; diff --git a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts index f7966aceeeb..ec1a6437ba0 100644 --- a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts +++ b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts @@ -14,6 +14,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionUntrustedWorkspaceSupport } from 'vs/base/common/product'; import { Disposable } from 'vs/base/common/lifecycle'; import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { isBoolean } from 'vs/base/common/types'; export const IExtensionManifestPropertiesService = createDecorator('extensionManifestPropertiesService'); @@ -171,8 +172,14 @@ export class ExtensionManifestPropertiesService extends Disposable implements IE } // check the manifest - if (manifest.capabilities?.virtualWorkspaces !== undefined) { - return manifest.capabilities?.virtualWorkspaces; + const virtualWorkspaces = manifest.capabilities?.virtualWorkspaces; + if (isBoolean(virtualWorkspaces)) { + return virtualWorkspaces; + } else if (virtualWorkspaces) { + const supported = virtualWorkspaces.supported; + if (isBoolean(supported) || supported === 'limited') { + return supported; + } } // check default from product diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 91eac5e38c3..4ebd9782b43 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -426,8 +426,28 @@ export const schema: IJSONSchema = { properties: { virtualWorkspaces: { description: nls.localize('vscode.extension.capabilities.virtualWorkspaces', "Declares whether the extension should be enabled in virtual workspaces. A virtual workspace is a workspace which is not backed by any on-disk resources. When false, this extension will be automatically disabled in virtual workspaces. Default is true."), - type: 'boolean', - default: true + type: ['boolean', 'object'], + defaultSnippets: [ + { label: 'limited', body: { supported: '${1:limited}', description: '${2}' } }, + { label: 'false', body: { supported: false, description: '${2}' } }, + ], + default: true.valueOf, + properties: { + supported: { + markdownDescription: nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported', "Declares the level of support for virtual workspaces by the extension."), + type: ['string', 'boolean'], + enum: ['limited', true, false], + enumDescriptions: [ + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.limited', "The extension will be enabled in virtual workspaces with some functionality disabled."), + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.true', "The extension will be enabled in virtual workspaces with all functionality enabled."), + nls.localize('vscode.extension.capabilities.virtualWorkspaces.supported.false', "The extension will not be enabled in virtual workspaces."), + ] + }, + description: { + type: 'string', + markdownDescription: nls.localize('vscode.extension.capabilities.virtualWorkspaces.description', "A description of how virtual workspaces affects the extensions behavior and why it is needed. This only applies when `supported` is not `true`."), + } + } }, untrustedWorkspaces: { description: nls.localize('vscode.extension.capabilities.untrustedWorkspaces', 'Declares how the extension should be handled in untrusted workspaces.'), From 32a06b9be2c8274d03bc652dd2f40f95e0ef52c1 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 21 May 2021 15:37:35 +0200 Subject: [PATCH 253/270] [typescript] add limited description --- extensions/typescript-language-features/package.json | 5 ++++- extensions/typescript-language-features/package.nls.json | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 1e089784b79..fbf6217c68c 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -9,7 +9,10 @@ "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "enableProposedApi": true, "capabilities": { - "virtualWorkspaces": true, + "virtualWorkspaces": { + "supported": "limited", + "description": "%virtualWorkspaces%" + }, "untrustedWorkspaces": { "supported": "limited", "description": "%workspaceTrust%", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 3f400cf4abe..85c896bef3c 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -2,6 +2,7 @@ "displayName": "TypeScript and JavaScript Language Features", "description": "Provides rich language support for JavaScript and TypeScript.", "workspaceTrust": "The extension requires workspace trust when the workspace version is used because it executes code specified by the workspace.", + "virtualWorkspaces": "In virtual workspaces, resolving and finding references across files is not supported.", "reloadProjects.title": "Reload Project", "configuration.typescript": "TypeScript", "configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.", From fc0b6f5e5ab63d154ecd354a00bae7ae1b41bf77 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 21 May 2021 16:01:37 +0200 Subject: [PATCH 254/270] fixes #122653 --- src/vs/base/browser/globalMouseMoveMonitor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/globalMouseMoveMonitor.ts b/src/vs/base/browser/globalMouseMoveMonitor.ts index 4911b59e080..6745aafbb13 100644 --- a/src/vs/base/browser/globalMouseMoveMonitor.ts +++ b/src/vs/base/browser/globalMouseMoveMonitor.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { IframeUtils } from 'vs/base/browser/iframe'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { isIOS } from 'vs/base/common/platform'; export interface IStandardMouseMoveEventData { leftButton: boolean; @@ -88,7 +89,7 @@ export class GlobalMouseMoveMonitor implements I this._onStopCallback = onStopCallback; const windowChain = IframeUtils.getSameOriginWindowChain(); - const mouseMove = 'mousemove'; + const mouseMove = isIOS ? 'pointermove' : 'mousemove'; // Safari sends wrong event, workaround for #122653 const mouseUp = 'mouseup'; const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document); From 045e5d2f56884f4732c168ff8e43578cb5703a03 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 21 May 2021 10:02:55 -0400 Subject: [PATCH 255/270] Store conflicting defaults in storage service (#124366) * Revert "Revert "Closes #122433"" This reverts commit 29c61570a5b9a669f777bb28b5acd5c37d99edbe. * Get rid of ? to see if minifier is happy * Bump ES build --- build/package.json | 2 +- build/yarn.lock | 8 ++--- .../editor/browser/editorOverrideService.ts | 35 +++++++++++++------ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/build/package.json b/build/package.json index 3170776b039..d390717cb2d 100644 --- a/build/package.json +++ b/build/package.json @@ -42,7 +42,7 @@ "colors": "^1.4.0", "commander": "^7.0.0", "electron-osx-sign": "^0.4.16", - "esbuild": "^0.8.30", + "esbuild": "^0.12.1", "fs-extra": "^9.1.0", "got": "11.8.1", "iconv-lite-umd": "0.6.8", diff --git a/build/yarn.lock b/build/yarn.lock index fb31e52e1df..e45d9619d70 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -992,10 +992,10 @@ entities@^1.1.1, entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== -esbuild@^0.8.30: - version "0.8.30" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.30.tgz#3d057ff9ffe6d5d30bccb0afe8cc92a2e69622d3" - integrity sha512-gCJQYUMO9QNrfpNOIiCnFoX41nWiPFCvURBQF+qWckyJ7gmw2xCShdKCXvS+RZcQ5krcxEOLIkzujqclePKhfw== +esbuild@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.1.tgz#f652d5b3b9432dbb42fc2c034ddd62360296e03d" + integrity sha512-WfQ00MKm/Y4ysz1u9PCUAsV66k5lbrcEvS6aG9jhBIavpB94FBdaWeBkaZXxCZB4w+oqh+j4ozJFWnnFprOXbg== eslint-scope@^5.0.0: version "5.0.0" diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 990a4acf6d9..13a06d4fdc5 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -40,9 +40,13 @@ type ContributionPoints = Array; export class EditorOverrideService extends Disposable implements IEditorOverrideService { readonly _serviceBrand: undefined; + // Constants private static readonly configureDefaultID = 'promptOpenWith.configureDefault'; - private _contributionPoints: Map = new Map(); private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; + private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; + + // Data Stores + private _contributionPoints: Map = new Map(); private cache: Set | undefined; constructor( @@ -129,8 +133,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } const input = await this.doOverrideEditorInput(editor, options, group, selectedContribution); if (conflictingDefault && input) { - // Wait one second to give the user ample time to see the current editor then ask them to configure a default - this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); + // Show the conflicting default dialog + await this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); } // Add the group as we might've changed it with the quickpick @@ -367,12 +371,23 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } private async doHandleConflictingDefaults(editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { - const makeCurrentEditorDefault = () => { - const viewType = currentEditor.viewType; - if (viewType) { - this.updateUserAssociations(`*${extname(currentEditor.resource!)}`, viewType); - } + type StoredChoice = { + [key: string]: string[]; }; + const contributionPoints = this.findMatchingContributions(currentEditor.resource!); + const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorOverrideService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); + const globForResource = `*${extname(currentEditor.resource!)}`; + // Writes to the storage service that a choice has been made for the currently installed editors + const writeCurrentEditorsToStorage = () => { + storedChoices[globForResource] = []; + contributionPoints.forEach(contrib => storedChoices[globForResource].push(contrib.editorInfo.id)); + this.storageService.store(EditorOverrideService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); + }; + + // If the user has already made a choice for this editor we don't want to ask them again + if (storedChoices[globForResource] && storedChoices[globForResource].find(editorID => editorID === currentEditor.viewType)) { + return; + } const handle = this.notificationService.prompt(Severity.Warning, localize('editorOverride.conflictingDefaults', 'There are multiple default editors available for the resource.'), @@ -400,12 +415,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }, { label: localize('editorOverride.keepDefault', 'Keep {0}', editorName), - run: makeCurrentEditorDefault + run: writeCurrentEditorsToStorage } ]); // If the user pressed X we assume they want to keep the current editor as default const onCloseListener = handle.onDidClose(() => { - makeCurrentEditorDefault(); + writeCurrentEditorsToStorage(); onCloseListener.dispose(); }); } From 49e96be2fc1e181d068d81dbe6b8b0fdef9c5aff Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 16:47:37 +0200 Subject: [PATCH 256/270] :lipstick: prefer `readonly` over `public readonly` --- src/vs/code/browser/workbench/workbench.ts | 4 ++-- src/vs/platform/files/common/files.ts | 4 ++-- src/vs/workbench/browser/dnd.ts | 4 ++-- src/vs/workbench/browser/parts/editor/editorPane.ts | 2 +- .../parts/notifications/notificationsActions.ts | 2 +- src/vs/workbench/common/editor/binaryEditorModel.ts | 10 ++++------ src/vs/workbench/common/editor/diffEditorInput.ts | 4 ++-- src/vs/workbench/common/editor/resourceEditorInput.ts | 2 +- src/vs/workbench/services/activity/common/activity.ts | 8 ++++---- .../services/textfile/common/textFileEditorModel.ts | 2 +- .../untitled/common/untitledTextEditorInput.ts | 2 +- .../untitled/common/untitledTextEditorModel.ts | 4 ++-- .../services/workingCopy/common/resourceWorkingCopy.ts | 2 +- src/vs/workbench/test/common/workbenchTestServices.ts | 2 +- 14 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 4aebea3e0da..9c222a0048e 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -278,8 +278,8 @@ class WorkspaceProvider implements IWorkspaceProvider { readonly trusted = true; constructor( - public readonly workspace: IWorkspace, - public readonly payload: object + readonly workspace: IWorkspace, + readonly payload: object ) { } async open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 4f02e2945d2..5d09c7e26d5 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -488,7 +488,7 @@ export enum FileSystemProviderErrorCode { export class FileSystemProviderError extends Error { - constructor(message: string, public readonly code: FileSystemProviderErrorCode) { + constructor(message: string, readonly code: FileSystemProviderErrorCode) { super(message); } } @@ -605,7 +605,7 @@ export class FileOperationEvent { constructor(resource: URI, operation: FileOperation.DELETE); constructor(resource: URI, operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY, target: IFileStatWithMetadata); - constructor(public readonly resource: URI, public readonly operation: FileOperation, public readonly target?: IFileStatWithMetadata) { } + constructor(readonly resource: URI, readonly operation: FileOperation, readonly target?: IFileStatWithMetadata) { } isOperation(operation: FileOperation.DELETE): boolean; isOperation(operation: FileOperation.MOVE | FileOperation.COPY | FileOperation.CREATE): this is { readonly target: IFileStatWithMetadata }; diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 441bb0124d9..17c55ea7b13 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -44,12 +44,12 @@ interface ISerializedDraggedResource { export class DraggedEditorIdentifier { - constructor(public readonly identifier: IEditorIdentifier) { } + constructor(readonly identifier: IEditorIdentifier) { } } export class DraggedEditorGroupIdentifier { - constructor(public readonly identifier: GroupIdentifier) { } + constructor(readonly identifier: GroupIdentifier) { } } interface IDraggedEditorProps { diff --git a/src/vs/workbench/browser/parts/editor/editorPane.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts index 081a0a8ae31..5af2b24f169 100644 --- a/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -193,7 +193,7 @@ export class EditorMemento implements IEditorMemento { private editorDisposables: Map | undefined; constructor( - public readonly id: string, + readonly id: string, private key: string, private memento: MementoObject, private limit: number, diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index a7af76e8d88..27fc5cdb963 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -122,7 +122,7 @@ export class ConfigureNotificationAction extends Action { constructor( id: string, label: string, - public readonly configurationActions: readonly IAction[] + readonly configurationActions: readonly IAction[] ) { super(id, label, ThemeIcon.asClassName(configureIcon)); } diff --git a/src/vs/workbench/common/editor/binaryEditorModel.ts b/src/vs/workbench/common/editor/binaryEditorModel.ts index 98bbf9360ba..b591076f47c 100644 --- a/src/vs/workbench/common/editor/binaryEditorModel.ts +++ b/src/vs/workbench/common/editor/binaryEditorModel.ts @@ -12,20 +12,18 @@ import { MIME_BINARY } from 'vs/base/common/mime'; * An editor model that just represents a resource that can be loaded. */ export class BinaryEditorModel extends EditorModel { + + private readonly mime = MIME_BINARY; + private size: number | undefined; private etag: string | undefined; - private readonly mime: string; constructor( - public readonly resource: URI, + readonly resource: URI, private readonly name: string, @IFileService private readonly fileService: IFileService ) { super(); - - this.resource = resource; - this.name = name; - this.mime = MIME_BINARY; } /** diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 7a21a642408..53800a77d3b 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -32,8 +32,8 @@ export class DiffEditorInput extends SideBySideEditorInput { constructor( name: string | undefined, description: string | undefined, - public readonly originalInput: EditorInput, - public readonly modifiedInput: EditorInput, + readonly originalInput: EditorInput, + readonly modifiedInput: EditorInput, private readonly forceOpenAsBinary: boolean | undefined, @ILabelService private readonly labelService: ILabelService, @IFileService private readonly fileService: IFileService diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 83fc944940e..dcc12fdcac3 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -19,7 +19,7 @@ export abstract class AbstractResourceEditorInput extends EditorInput implements get preferredResource(): URI { return this._preferredResource; } constructor( - public readonly resource: URI, + readonly resource: URI, preferredResource: URI | undefined, @ILabelService protected readonly labelService: ILabelService, @IFileService protected readonly fileService: IFileService diff --git a/src/vs/workbench/services/activity/common/activity.ts b/src/vs/workbench/services/activity/common/activity.ts index 8eda8c67e8e..1f9fc1f0d1f 100644 --- a/src/vs/workbench/services/activity/common/activity.ts +++ b/src/vs/workbench/services/activity/common/activity.ts @@ -46,7 +46,7 @@ export interface IBadge { class BaseBadge implements IBadge { - constructor(public readonly descriptorFn: (arg: any) => string) { + constructor(readonly descriptorFn: (arg: any) => string) { this.descriptorFn = descriptorFn; } @@ -57,7 +57,7 @@ class BaseBadge implements IBadge { export class NumberBadge extends BaseBadge { - constructor(public readonly number: number, descriptorFn: (num: number) => string) { + constructor(readonly number: number, descriptorFn: (num: number) => string) { super(descriptorFn); this.number = number; @@ -70,13 +70,13 @@ export class NumberBadge extends BaseBadge { export class TextBadge extends BaseBadge { - constructor(public readonly text: string, descriptorFn: () => string) { + constructor(readonly text: string, descriptorFn: () => string) { super(descriptorFn); } } export class IconBadge extends BaseBadge { - constructor(public readonly icon: ThemeIcon, descriptorFn: () => string) { + constructor(readonly icon: ThemeIcon, descriptorFn: () => string) { super(descriptorFn); } } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 0b141f6a648..c98a79f6c3f 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -94,7 +94,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private inErrorMode = false; constructor( - public readonly resource: URI, + readonly resource: URI, private preferredEncoding: string | undefined, // encoding as chosen by the user private preferredMode: string | undefined, // mode as chosen by the user @IModeService modeService: IModeService, diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts index f7c7af6cc9e..435ffae63e2 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts @@ -26,7 +26,7 @@ export class UntitledTextEditorInput extends AbstractTextResourceEditorInput imp private modelResolve: Promise | undefined = undefined; constructor( - public readonly model: IUntitledTextEditorModel, + readonly model: IUntitledTextEditorModel, @ITextFileService textFileService: ITextFileService, @ILabelService labelService: ILabelService, @IEditorService editorService: IEditorService, diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 7f4557bdde7..aa0deaa39b3 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -114,8 +114,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt constructor( - public readonly resource: URI, - public readonly hasAssociatedFilePath: boolean, + readonly resource: URI, + readonly hasAssociatedFilePath: boolean, private readonly initialValue: string | undefined, private preferredMode: string | undefined, private preferredEncoding: string | undefined, diff --git a/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts index f278448fa6d..52e1f05c6fb 100644 --- a/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts @@ -42,7 +42,7 @@ export interface IResourceWorkingCopy extends IWorkingCopy, IDisposable { export abstract class ResourceWorkingCopy extends Disposable implements IResourceWorkingCopy { constructor( - public readonly resource: URI, + readonly resource: URI, @IFileService protected readonly fileService: IFileService ) { super(); diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index d43993d9425..b2ddf026545 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -145,7 +145,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { private dirty = false; - constructor(public readonly resource: URI, isDirty = false, public readonly typeId = 'testWorkingCopyType') { + constructor(readonly resource: URI, isDirty = false, readonly typeId = 'testWorkingCopyType') { super(); this.dirty = isDirty; From c650993dd39ec625cad32e1fc23719cdf4caaa51 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 21 May 2021 14:56:17 +0200 Subject: [PATCH 257/270] Add `ResolvedOptions.isTrusted` --- .../remote/common/remoteAuthorityResolver.ts | 8 +--- src/vs/vscode.proposed.d.ts | 8 +--- .../workbench/api/common/extHost.api.impl.ts | 2 - .../api/common/extHostExtensionService.ts | 2 +- .../electron-browser/extensionService.ts | 37 +------------------ 5 files changed, 5 insertions(+), 52 deletions(-) diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index 52902edfcbc..f14dfa930cb 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -15,15 +15,9 @@ export interface ResolvedAuthority { readonly connectionToken: string | undefined; } -export enum RemoteTrustOption { - Unknown = 0, - DisableTrust = 1, - MachineTrusted = 2 -} - export interface ResolvedOptions { readonly extensionHostEnv?: { [key: string]: string | null }; - readonly trust?: RemoteTrustOption; + readonly isTrusted?: boolean; } export interface TunnelDescription { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 522ad85e61c..d605d7dc83d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -80,16 +80,10 @@ declare module 'vscode' { constructor(host: string, port: number, connectionToken?: string); } - export enum RemoteTrustOption { - Unknown = 0, - DisableTrust = 1, - MachineTrusted = 2 - } - export interface ResolvedOptions { extensionHostEnv?: { [key: string]: string | null; }; - trust?: RemoteTrustOption; + isTrusted?: boolean; } export interface TunnelOptions { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ad6422e744a..533f64034da 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -84,7 +84,6 @@ import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; -import { RemoteTrustOption } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers'; import { Schemas } from 'vs/base/common/network'; @@ -1250,7 +1249,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InlayHint: extHostTypes.InlayHint, InlayHintKind: extHostTypes.InlayHintKind, RemoteAuthorityResolverError: extHostTypes.RemoteAuthorityResolverError, - RemoteTrustOption: RemoteTrustOption, ResolvedAuthority: extHostTypes.ResolvedAuthority, SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, ExtensionRuntime: extHostTypes.ExtensionRuntime, diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 324f7cf3297..978751f4144 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -668,7 +668,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme }; const options: ResolvedOptions = { extensionHostEnv: result.extensionHostEnv, - trust: result.trust + isTrusted: result.isTrusted }; return { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 46850e22397..a97a93b7c98 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -15,7 +15,7 @@ import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsSc import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteTrustOption, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -42,13 +42,9 @@ import { Schemas } from 'vs/base/common/network'; import { ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { updateProxyConfigurationsScope } from 'vs/platform/request/common/request'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { Codicon } from 'vs/base/common/codicons'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -const MACHINE_PROMPT = false; - export class ExtensionService extends AbstractExtensionService implements IExtensionService { private readonly _enableLocalWebWorker: boolean; @@ -75,7 +71,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, @IExtensionGalleryService private readonly _extensionGalleryService: IExtensionGalleryService, @ILogService private readonly _logService: ILogService, - @IDialogService private readonly _dialogService: IDialogService, @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { @@ -368,36 +363,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten return; } - let promptForMachineTrust = MACHINE_PROMPT; - - if (resolverResult.options?.trust === RemoteTrustOption.DisableTrust) { - promptForMachineTrust = false; + if (resolverResult.options?.isTrusted) { await this._workspaceTrustManagementService.setWorkspaceTrust(true); - } else if (resolverResult.options?.trust === RemoteTrustOption.MachineTrusted) { - promptForMachineTrust = false; - } - - if (promptForMachineTrust) { - const dialogResult = await this._dialogService.show( - Severity.Info, - nls.localize('machineTrustQuestion', "Do you trust the machine you're connecting to?"), - [nls.localize('yes', "Yes, connect."), nls.localize('no', "No, do not connect.")], - { - cancelId: 1, - custom: { - icon: Codicon.remoteExplorer - }, - // checkbox: { label: nls.localize('remember', "Remember my choice"), checked: true } - } - ); - - if (dialogResult.choice !== 0) { - // Did not confirm trust - this._notificationService.notify({ severity: Severity.Warning, message: nls.localize('trustFailure', "Refused to connect to untrusted machine.") }); - // Proceed with the local extension host - await this._startLocalExtensionHost(localExtensions); - return; - } } // set the resolved authority From 85f518b2552e1270cf76f0613ea2f0bd037388c7 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 21 May 2021 16:47:57 +0200 Subject: [PATCH 258/270] Add `RemoteAuthorityResolver.getCanonicalURI` --- .../vscode-test-resolver/src/extension.ts | 3 + .../browser/remoteAuthorityResolverService.ts | 7 ++ .../remote/common/remoteAuthorityResolver.ts | 10 +++ .../remoteAuthorityResolverService.ts | 70 ++++++++++++------- src/vs/vscode.proposed.d.ts | 17 +++++ .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostExtensionService.ts | 35 ++++++++-- .../extensions/common/extensionHostManager.ts | 10 +++ .../electron-browser/extensionService.ts | 14 ++++ 9 files changed, 139 insertions(+), 28 deletions(-) diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index c338957146b..16a3d0cca57 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -216,6 +216,9 @@ export function activate(context: vscode.ExtensionContext) { } const authorityResolverDisposable = vscode.workspace.registerRemoteAuthorityResolver('test', { + async getCanonicalURI(uri: vscode.Uri): Promise { + return vscode.Uri.file(uri.path); + }, resolve(_authority: string): Thenable { return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, diff --git a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts index 3d5d29b8e00..e7f177ceb17 100644 --- a/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts @@ -40,6 +40,10 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot return this._cache.get(authority)!; } + async getCanonicalURI(uri: URI): Promise { + return uri; + } + getConnectionData(authority: string): IRemoteConnectionData | null { if (!this._cache.has(authority)) { return null; @@ -76,4 +80,7 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot RemoteAuthorities.setConnectionToken(authority, connectionToken); this._onDidChangeConnectionData.fire(); } + + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void { + } } diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index f14dfa930cb..9a310877998 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -5,6 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; export const IRemoteAuthorityResolverService = createDecorator('remoteAuthorityResolverService'); @@ -92,9 +93,18 @@ export interface IRemoteAuthorityResolverService { resolveAuthority(authority: string): Promise; getConnectionData(authority: string): IRemoteConnectionData | null; + /** + * Get the canonical URI for a `vscode-remote://` URI. + * + * **NOTE**: This can throw e.g. in cases where there is no resolver installed for the specific remote authority. + * + * @param uri The `vscode-remote://` URI + */ + getCanonicalURI(uri: URI): Promise; _clearResolvedAuthority(authority: string): void; _setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void; _setResolvedAuthorityError(authority: string, err: any): void; _setAuthorityConnectionToken(authority: string, connectionToken: string): void; + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void; } diff --git a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts index 41529a21aee..93081c02147 100644 --- a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts @@ -8,22 +8,27 @@ import * as errors from 'vs/base/common/errors'; import { RemoteAuthorities } from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; -class PendingResolveAuthorityRequest { +class PendingPromise { + public readonly promise: Promise; + public readonly input: I; + public result: R | null; + private _resolve!: (value: R) => void; + private _reject!: (err: any) => void; - public value: ResolverResult | null; - - constructor( - private readonly _resolve: (value: ResolverResult) => void, - private readonly _reject: (err: any) => void, - public readonly promise: Promise, - ) { - this.value = null; + constructor(request: I) { + this.input = request; + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + this.result = null; } - resolve(value: ResolverResult): void { - this.value = value; - this._resolve(this.value); + resolve(result: R): void { + this.result = result; + this._resolve(this.result); } reject(err: any): void { @@ -38,40 +43,50 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot private readonly _onDidChangeConnectionData = this._register(new Emitter()); public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event; - private readonly _resolveAuthorityRequests: Map; + private readonly _resolveAuthorityRequests: Map>; private readonly _connectionTokens: Map; + private readonly _canonicalURIRequests: Map>; + private _canonicalURIProvider: ((uri: URI) => Promise) | null; constructor() { super(); - this._resolveAuthorityRequests = new Map(); + this._resolveAuthorityRequests = new Map>(); this._connectionTokens = new Map(); + this._canonicalURIRequests = new Map>(); + this._canonicalURIProvider = null; } resolveAuthority(authority: string): Promise { if (!this._resolveAuthorityRequests.has(authority)) { - let resolve: (value: ResolverResult) => void; - let reject: (err: any) => void; - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - this._resolveAuthorityRequests.set(authority, new PendingResolveAuthorityRequest(resolve!, reject!, promise)); + this._resolveAuthorityRequests.set(authority, new PendingPromise(authority)); } return this._resolveAuthorityRequests.get(authority)!.promise; } + async getCanonicalURI(uri: URI): Promise { + const key = uri.toString(); + if (!this._canonicalURIRequests.has(key)) { + const request = new PendingPromise(uri); + if (this._canonicalURIProvider) { + this._canonicalURIProvider(request.input).then((uri) => request.resolve(uri), (err) => request.reject(err)); + } + this._canonicalURIRequests.set(key, request); + } + return this._canonicalURIRequests.get(key)!.promise; + } + getConnectionData(authority: string): IRemoteConnectionData | null { if (!this._resolveAuthorityRequests.has(authority)) { return null; } const request = this._resolveAuthorityRequests.get(authority)!; - if (!request.value) { + if (!request.result) { return null; } const connectionToken = this._connectionTokens.get(authority); return { - host: request.value.authority.host, - port: request.value.authority.port, + host: request.result.authority.host, + port: request.result.authority.port, connectionToken: connectionToken }; } @@ -107,4 +122,11 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot RemoteAuthorities.setConnectionToken(authority, connectionToken); this._onDidChangeConnectionData.fire(); } + + _setCanonicalURIProvider(provider: (uri: URI) => Promise): void { + this._canonicalURIProvider = provider; + this._canonicalURIRequests.forEach((value) => { + this._canonicalURIProvider!(value.input).then((uri) => value.resolve(uri), (err) => value.reject(err)); + }); + } } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index d605d7dc83d..ac28efdd68c 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -144,7 +144,24 @@ declare module 'vscode' { } export interface RemoteAuthorityResolver { + /** + * Resolve the authority part of the current opened `vscode-remote://` URI. + * + * This method will be invoked once during the startup of VS Code and again each time + * VS Code detects a disconnection. + * + * @param authority The authority part of the current opened `vscode-remote://` URI. + * @param context A context indicating if this is the first call or a subsequent call. + */ resolve(authority: string, context: RemoteAuthorityResolverContext): ResolverResult | Thenable; + + /** + * Get the canonical URI (if applicable) for a `vscode-remote://` URI. + * + * @returns The canonical URI or undefined if the uri is already canonical. + */ + getCanonicalURI?(uri: Uri): ProviderResult; + /** * Can be optionally implemented if the extension can forward ports better than the core. * When not implemented, the core will use its default forwarding logic. diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 427b2d2e342..bc9c5763523 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1297,6 +1297,7 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; + $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; $extensionTestsExecute(): Promise; $extensionTestsExit(code: number): Promise; diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 978751f4144..abb761d9dbe 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -7,10 +7,10 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as performance from 'vs/base/common/performance'; import { originalFSPath, joinPath } from 'vs/base/common/resources'; -import { Barrier, timeout } from 'vs/base/common/async'; +import { asPromise, Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostExtensionServiceShape, IInitData, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape, IResolveAuthorityResult } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; @@ -631,7 +631,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // -- called by main thread - public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + private async _activateAndGetResolver(remoteAuthority: string): Promise<{ authorityPrefix: string; resolver: vscode.RemoteAuthorityResolver | undefined; }> { const authorityPlusIndex = remoteAuthority.indexOf('+'); if (authorityPlusIndex === -1) { throw new Error(`Not an authority that can be resolved!`); @@ -641,7 +641,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme await this._almostReadyToRunExtensions.wait(); await this._activateByEvent(`onResolveRemoteAuthority:${authorityPrefix}`, false); - const resolver = this._resolvers[authorityPrefix]; + return { authorityPrefix, resolver: this._resolvers[authorityPrefix] }; + } + + public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + + const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority); if (!resolver) { return { type: 'error', @@ -695,6 +700,28 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } + public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise { + + const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority); + if (!resolver) { + throw new Error(`Cannot get canonical URI because no remote extension is installed to resolve ${authorityPrefix}`); + } + + const uri = URI.revive(uriComponents); + + if (typeof resolver.getCanonicalURI === 'undefined') { + // resolver cannot compute canonical URI + return uri; + } + + const result = await asPromise(() => resolver.getCanonicalURI!(uri)); + if (!result) { + return uri; + } + + return result; + } + public $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise { this._registry.keepOnly(enabledExtensionIds); return this._startExtensionHost(); diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index b363ff4c1ff..b67841044c3 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -24,6 +24,7 @@ import { IExtensionHost, ExtensionHostKind, ActivationKind } from 'vs/workbench/ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { timeout } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -285,6 +286,15 @@ export class ExtensionHostManager extends Disposable { } } + public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { + const proxy = await this._getProxy(); + if (!proxy) { + throw new Error(`Cannot resolve canonical URI`); + } + const result = await proxy.$getCanonicalURI(remoteAuthority, uri); + return URI.revive(result); + } + public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise { const proxy = await this._getProxy(); if (!proxy) { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index a97a93b7c98..90876b4ae4f 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -343,6 +343,16 @@ export class ExtensionService extends AbstractExtensionService implements IExten let remoteExtensions: IExtensionDescription[] = []; if (remoteAuthority) { + + this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => { + if (uri.authority !== remoteAuthority) { + // The current remote authority resolver cannot give the canonical URI for this URI + return uri; + } + const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; + return localProcessExtensionHost.getCanonicalURI(remoteAuthority, uri); + }); + let resolverResult: ResolverResult; try { @@ -397,6 +407,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten } updateProxyConfigurationsScope(remoteEnv.useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE); + } else { + + this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => uri); + } await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv, remoteExtensions); From c5b9b6c48c7192d6708ae2840d0a8de58c5bd600 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 21 May 2021 16:50:40 +0200 Subject: [PATCH 259/270] Fix missing async --- src/vs/platform/workspace/common/workspaceTrust.ts | 2 +- .../contrib/workspace/browser/workspace.contribution.ts | 4 ++-- .../contrib/workspace/browser/workspaceTrustEditor.ts | 8 ++++---- .../workspaces/browser/abstractWorkspaceEditingService.ts | 6 +++--- .../services/workspaces/common/workspaceTrust.ts | 4 ++-- .../workspaces/test/common/testWorkspaceTrustService.ts | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 343273375c1..ac579788e62 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -46,7 +46,7 @@ export interface IWorkspaceTrustManagementService { addWorkspaceTrustTransitionParticipant(participant: IWorkspaceTrustTransitionParticipant): IDisposable; isWorkpaceTrusted(): boolean; canSetParentFolderTrust(): boolean; - setParentFolderTrust(trusted: boolean): void; + setParentFolderTrust(trusted: boolean): Promise; canSetWorkspaceTrust(): boolean; setWorkspaceTrust(trusted: boolean): Promise; getUriTrustInfo(folder: URI): IWorkspaceTrustUriInfo; diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index a05c09c0af7..ee5b0fac7ee 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -144,7 +144,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben switch (result.choice) { case 0: if (result.checkboxChecked) { - this.workspaceTrustManagementService.setParentFolderTrust(true); + await this.workspaceTrustManagementService.setParentFolderTrust(true); } else { await this.workspaceTrustRequestService.completeRequest(true); } @@ -441,7 +441,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben ); // Mark added/changed folders as trusted - this.workspaceTrustManagementService.setUrisTrust(addedFoldersTrustInfo.map(i => i.uri), result.choice === 0); + await this.workspaceTrustManagementService.setUrisTrust(addedFoldersTrustInfo.map(i => i.uri), result.choice === 0); resolve(); } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 92049a21903..d4157f041ba 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -230,8 +230,8 @@ class WorkspaceTrustedUrisTable extends Disposable { this._onDidRejectEdit.fire(item); } - delete(item: ITrustedUriItem) { - this.workspaceTrustManagementService.setUrisTrust([item.uri], false); + async delete(item: ITrustedUriItem) { + await this.workspaceTrustManagementService.setUrisTrust([item.uri], false); this._onDelete.fire(item); } @@ -316,8 +316,8 @@ class TrustedUriActionsColumnRenderer implements ITableRenderer { - this.table.delete(item); + run: async () => { + await this.table.delete(item); } }; } diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 282d4872fbb..41b2a0def5c 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -258,7 +258,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi await this.textFileService.create([{ resource: targetConfigPathURI, value: newRawWorkspaceContents, options: { overwrite: true } }]); // Set trust for the workspace file - this.trustWorkspaceConfiguration(targetConfigPathURI); + await this.trustWorkspaceConfiguration(targetConfigPathURI); } protected async saveWorkspace(workspace: IWorkspaceIdentifier): Promise { @@ -359,9 +359,9 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi return this.jsonEditingService.write(toWorkspace.configPath, [{ path: ['settings'], value: targetWorkspaceConfiguration }], true); } - private trustWorkspaceConfiguration(configPathURI: URI): void { + private async trustWorkspaceConfiguration(configPathURI: URI): Promise { if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.workspaceTrustManagementService.isWorkpaceTrusted()) { - this.workspaceTrustManagementService.setUrisTrust([configPathURI], true); + await this.workspaceTrustManagementService.setUrisTrust([configPathURI], true); } } diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index b0ae61b0964..00ece8fe9b5 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -290,12 +290,12 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork return this._trustState.isTrusted ?? false; } - setParentFolderTrust(trusted: boolean): void { + async setParentFolderTrust(trusted: boolean): Promise { const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); - this.setUrisTrust([URI.file(parentPath)], trusted); + await this.setUrisTrust([URI.file(parentPath)], trusted); } } diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts index 14282c50ac2..6e6e68772ce 100644 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts @@ -40,7 +40,7 @@ export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManag throw new Error('Method not implemented.'); } - setParentFolderTrust(trusted: boolean): void { + setParentFolderTrust(trusted: boolean): Promise { throw new Error('Method not implemented.'); } From 8aff8020daeb3a9bb9a87bb95add8670c192b514 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 21 May 2021 16:59:46 +0200 Subject: [PATCH 260/270] Improve guard against unexpected URI call --- .../services/extensions/electron-browser/extensionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 90876b4ae4f..67d5ce01398 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -345,7 +345,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten if (remoteAuthority) { this._remoteAuthorityResolverService._setCanonicalURIProvider(async (uri) => { - if (uri.authority !== remoteAuthority) { + if (uri.scheme !== Schemas.vscodeRemote || uri.authority !== remoteAuthority) { // The current remote authority resolver cannot give the canonical URI for this URI return uri; } From 8f093359f0776b5a44b4a6785a93e036e7b1ab4e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 17:02:56 +0200 Subject: [PATCH 261/270] editors - simplify some serialized editor input --- .../electron-sandbox/extensions.contribution.ts | 2 +- .../preferences/browser/preferences.contribution.ts | 12 ++++-------- .../contrib/welcome/page/browser/welcomePage.ts | 4 ++-- .../walkThrough/browser/editor/editorWalkThrough.ts | 4 ++-- .../workspace/browser/workspace.contribution.ts | 4 ++-- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index cba25e85507..6b0ba3cad76 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -36,7 +36,7 @@ class RuntimeExtensionsInputSerializer implements IEditorInputSerializer { serialize(editorInput: EditorInput): string { return ''; } - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { + deserialize(instantiationService: IInstantiationService): EditorInput { return RuntimeExtensionsInput.instance; } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 372b4a853e2..c0109b78a12 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -110,14 +110,10 @@ class KeybindingsEditorInputSerializer implements IEditorInputSerializer { } serialize(editorInput: EditorInput): string { - const input = editorInput; - return JSON.stringify({ - name: input.getName(), - typeId: input.typeId - }); + return ''; } - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { + deserialize(instantiationService: IInstantiationService): EditorInput { return instantiationService.createInstance(KeybindingsEditorInput); } } @@ -129,10 +125,10 @@ class SettingsEditor2InputSerializer implements IEditorInputSerializer { } serialize(input: SettingsEditor2Input): string { - return '{}'; + return ''; } - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): SettingsEditor2Input { + deserialize(instantiationService: IInstantiationService): SettingsEditor2Input { return instantiationService.createInstance(SettingsEditor2Input); } } diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index 8471fa8ea01..89702ffe4d1 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -748,10 +748,10 @@ export class WelcomeInputSerializer implements IEditorInputSerializer { } public serialize(editorInput: EditorInput): string { - return '{}'; + return ''; } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WalkThroughInput { + public deserialize(instantiationService: IInstantiationService): WalkThroughInput { return instantiationService.createInstance(WelcomePage) .editorInput; } diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts index 4c055d9f81e..44eeec072b2 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/editor/editorWalkThrough.ts @@ -55,10 +55,10 @@ export class EditorWalkThroughInputSerializer implements IEditorInputSerializer } public serialize(editorInput: EditorInput): string { - return '{}'; + return ''; } - public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WalkThroughInput { + public deserialize(instantiationService: IInstantiationService): WalkThroughInput { return instantiationService.createInstance(WalkThroughInput, inputOptions); } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index ee5b0fac7ee..e894eb2020d 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -469,10 +469,10 @@ class WorkspaceTrustEditorInputSerializer implements IEditorInputSerializer { } serialize(input: WorkspaceTrustEditorInput): string { - return '{}'; + return ''; } - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WorkspaceTrustEditorInput { + deserialize(instantiationService: IInstantiationService): WorkspaceTrustEditorInput { return instantiationService.createInstance(WorkspaceTrustEditorInput); } } From adf68a52d56dfa7b4d8a573a2fef682917f7f709 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 21 May 2021 17:54:05 +0200 Subject: [PATCH 262/270] editors - some type :lipstick: around setInput --- .../workbench/browser/parts/editor/sideBySideEditor.ts | 10 +++++----- src/vs/workbench/contrib/output/browser/outputView.ts | 4 ++-- .../contrib/preferences/browser/preferencesEditor.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index 5a857f4cc0a..d27103d4dbf 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -94,11 +94,11 @@ export class SideBySideEditor extends EditorPane { this.updateStyles(); } - override async setInput(newInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const oldInput = this.input as SideBySideEditorInput; - await super.setInput(newInput, options, context, token); + override async setInput(input: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + const oldInput = this.input; + await super.setInput(input, options, context, token); - return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, context, token); + return this.updateInput(oldInput, input, options, context, token); } override setOptions(options: EditorOptions | undefined): void { @@ -162,7 +162,7 @@ export class SideBySideEditor extends EditorPane { return this.secondaryEditorPane; } - private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + private async updateInput(oldInput: EditorInput | undefined, newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (!newInput.matches(oldInput)) { if (oldInput) { this.disposeEditors(); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 852f3659252..54e4474aa16 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -12,7 +12,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; @@ -221,7 +221,7 @@ export class OutputEditor extends AbstractTextResourceEditor { return channel ? nls.localize('outputViewWithInputAriaLabel', "{0}, Output panel", channel.label) : nls.localize('outputViewAriaLabel', "Output panel"); } - override async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override async setInput(input: TextResourceEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise {// const focus = !(options && options.preserveFocus); if (input.matches(this.input)) { return; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index 3b0f9656d56..71a30eac276 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -151,14 +151,14 @@ export class PreferencesEditor extends EditorPane { this.preferencesRenderers.editFocusedPreference(); } - override setInput(newInput: EditorInput, options: SettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + override setInput(input: PreferencesEditorInput, options: SettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise {// this.defaultSettingsEditorContextKey.set(true); this.defaultSettingsJSONEditorContextKey.set(true); if (options && options.query) { this.focusSearch(options.query); } - return super.setInput(newInput, options, context, token).then(() => this.updateInput(newInput as PreferencesEditorInput, options, context, token)); + return super.setInput(input, options, context, token).then(() => this.updateInput(input, options, context, token)); } layout(dimension: DOM.Dimension): void { @@ -205,7 +205,7 @@ export class PreferencesEditor extends EditorPane { } private updateInput(newInput: PreferencesEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - return this.sideBySidePreferencesWidget.setInput(newInput.secondary, newInput.primary, options, context, token).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { + return this.sideBySidePreferencesWidget.setInput(newInput.secondary, newInput.primary, options, context, token).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { if (token.isCancellationRequested) { return; } From 70c87f0db9b273b033fc7d404137f640f7d6f66f Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 21 May 2021 09:12:43 -0700 Subject: [PATCH 263/270] Fix #124240 --- .../src/features/validationProvider.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/extensions/php-language-features/src/features/validationProvider.ts b/extensions/php-language-features/src/features/validationProvider.ts index 162efdaa787..fb4111c874b 100644 --- a/extensions/php-language-features/src/features/validationProvider.ts +++ b/extensions/php-language-features/src/features/validationProvider.ts @@ -194,15 +194,17 @@ export default class PHPValidationProvider { if (vscode.workspace.isTrusted) { trigger(); } - } else if (this.config!.executableIsUserDefined !== undefined && !this.config!.executableIsUserDefined) { - const checkedExecutablePath = this.workspaceStore.get(Setting.CheckedExecutablePath, undefined); - if (!checkedExecutablePath || checkedExecutablePath !== this.config!.executable) { - if (await this.showCustomTrustDialog()) { - this.workspaceStore.update(Setting.CheckedExecutablePath, this.config!.executable); - vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true); - } else { - this.pauseValidation = true; - return; + } else { + if (this.config!.executableIsUserDefined !== undefined && !this.config!.executableIsUserDefined) { + const checkedExecutablePath = this.workspaceStore.get(Setting.CheckedExecutablePath, undefined); + if (!checkedExecutablePath || checkedExecutablePath !== this.config!.executable) { + if (await this.showCustomTrustDialog()) { + this.workspaceStore.update(Setting.CheckedExecutablePath, this.config!.executable); + vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true); + } else { + this.pauseValidation = true; + return; + } } } From 79dea51e79ac4bb48311a332adcada9a397bc59c Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 20 May 2021 22:34:35 -0700 Subject: [PATCH 264/270] Rewrite webview urls to be more url-ish ## Problem Webview uris currently have the form: ``` https://uuid.vscode-webview.net/vscode-resource/scheme/authority/path... ``` We have this syntax because we need to be able to recover the original scheme and authority of the resource in order to load it from disk However this syntax means that absolute urls don't behave as you'd expect. For example, if you have a webview that sets a `` to a document in the workspace, an absolute url `/abs/path.png` ends up being resolved to: ``` https://uuid.vscode-webview.net/abs/path.png ``` This drops the original scheme and authority, which prevents us from loading the resource ## Fix With this change, I've moved the original scheme and authority into the authority of the webview uri instead of the path: ``` https://scheme+authority.vscode-resource.uuid.vscode-webview.net/path... ``` With this change, absolute paths should correctly be resolved --- .../src/markdownEngine.ts | 8 ++-- src/vs/workbench/api/common/shared/webview.ts | 18 ++++--- .../webview/browser/baseWebviewElement.ts | 47 ++++++++++--------- .../contrib/webview/browser/pre/main.js | 5 +- .../webview/browser/pre/service-worker.js | 40 +++++++++------- .../contrib/webview/browser/webviewElement.ts | 19 +++++--- .../electron-browser/webviewElement.ts | 2 +- .../test/browser/api/extHostWebview.test.ts | 30 ++++++------ 8 files changed, 88 insertions(+), 81 deletions(-) diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 3d84a24ad30..688a4ff92e4 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -269,14 +269,12 @@ export class MarkdownEngine { if (uri.path[0] === '/') { const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!); if (root) { - const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({ + uri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({ fragment: uri.fragment, query: uri.query, + }).with({ + scheme: 'markdown-link' }); - - // Ensure fileUri is relative by prepending `/` so that it uses the element URI - // when resolving the absolute URL - uri = vscode.Uri.parse('markdown-link:' + '/' + fileUri.toString(true).replace(/^\S+?:/, fileUri.scheme)); } } diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index de315478caa..cf404c89bb7 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -20,13 +20,11 @@ export interface WebviewInitData { * This is hardcoded because we never expect to actually hit it. Instead these requests * should always go to a service worker. */ -export const webviewResourceAuthority = 'vscode-webview.net'; +export const webviewResourceBaseHost = 'vscode-webview.net'; -export const webviewResourceOrigin = (id: string) => `https://${id}.${webviewResourceAuthority}`; +export const webviewRootResourceAuthority = (id: string) => `vscode-resource.${id}.${webviewResourceBaseHost}`; -export const webviewResourceRoot = (id: string) => `${webviewResourceOrigin(id)}/vscode-resource/{{resource}}`; - -export const webviewGenericCspSource = `https://*.${webviewResourceAuthority}`; +export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; /** * Construct a uri that can load resources inside a webview @@ -35,7 +33,7 @@ export const webviewGenericCspSource = `https://*.${webviewResourceAuthority}`; * we know where to load the resource from (remote or truly local): * * ```txt - * scheme/resource-authority/path... + * ${scheme}+${resource-authority}.vscode-resource.${id}.vscode-webview.net/${path} * ``` * * @param uuid Unique id of the webview. @@ -59,10 +57,10 @@ export function asWebviewUri( }); } - const uri = webviewResourceRoot(uuid) - .replace('{{resource}}', resource.scheme + '/' + encodeURIComponent(resource.authority) + resource.path) - .replace('{{uuid}}', uuid); - return URI.parse(uri).with({ + return URI.from({ + scheme: Schemas.https, + authority: `${resource.scheme}+${encodeURIComponent(resource.authority)}.${webviewRootResourceAuthority(uuid)}`, + path: resource.path, fragment: resource.fragment, query: resource.query, }); diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index ed405e35c00..0d04c6358b9 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -25,7 +25,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; -import { asWebviewUri, webviewResourceOrigin } from 'vs/workbench/api/common/shared/webview'; +import { asWebviewUri, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/api/common/shared/webview'; import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/browser/resourceLoading'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { areWebviewContentOptionsEqual, WebviewContentOptions, WebviewExtensionDescription, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; @@ -239,21 +239,22 @@ export abstract class BaseWebview extends Disposable { }); })); - this._register(this.on(WebviewMessageChannels.loadResource, (entry: { id: number, path: string, query: string, ifNoneMatch?: string }) => { - const rawPath = entry.path; - // scheme / path-authority / ...path - const match = rawPath.match(/^\/([^\/]*)\/([^\/]*)(\/.+)$/); - if (!match) { - throw new Error('Could not parse resource url'); + this._register(this.on(WebviewMessageChannels.loadResource, (entry: { id: number, path: string, query: string, scheme: string, authority: string, ifNoneMatch?: string }) => { + try { + const uri = URI.from({ + scheme: entry.scheme, + authority: entry.authority, + path: entry.path, + query: entry.query, + }); + this.loadResource(entry.id, uri, entry.ifNoneMatch); + } catch (e) { + this._send('did-load-resource', { + id, + status: 404, + path: entry.path, + }); } - - const [_, scheme, pathAuthority, paths] = match; - - const uri = URI.parse(`${scheme}://${decodeURIComponent(pathAuthority)}${paths}`).with({ - query: decodeURIComponent(entry.query), - }); - - this.loadResource(entry.id, rawPath, uri, entry.ifNoneMatch); })); this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => { @@ -374,8 +375,8 @@ export abstract class BaseWebview extends Disposable { }); } - protected get webviewResourceOrigin(): string { - return webviewResourceOrigin(this.id); + protected get webviewRootResourceAuthority(): string { + return webviewRootResourceAuthority(this.id); } private rewriteVsCodeResourceUrls(value: string): string { @@ -443,7 +444,7 @@ export abstract class BaseWebview extends Disposable { contents: this.content.html, options: this.content.options, state: this.content.state, - resourceEndpoint: this.webviewResourceOrigin, + cspSource: webviewGenericCspSource, ...this.extraContentOptions }); } @@ -522,7 +523,7 @@ export abstract class BaseWebview extends Disposable { } } - private async loadResource(id: number, requestPath: string, uri: URI, ifNoneMatch: string | undefined) { + private async loadResource(id: number, uri: URI, ifNoneMatch: string | undefined) { try { const result = await loadLocalResource(uri, { ifNoneMatch, @@ -536,7 +537,7 @@ export abstract class BaseWebview extends Disposable { return this._send('did-load-resource', { id, status: 200, - path: requestPath, + path: uri.path, mime: result.mimeType, data: buffer, etag: result.etag, @@ -547,7 +548,7 @@ export abstract class BaseWebview extends Disposable { return this._send('did-load-resource', { id, status: 304, // not modified - path: requestPath, + path: uri.path, mime: result.mimeType, }); } @@ -556,7 +557,7 @@ export abstract class BaseWebview extends Disposable { return this._send('did-load-resource', { id, status: 401, // unauthorized - path: requestPath, + path: uri.path, }); } } @@ -567,7 +568,7 @@ export abstract class BaseWebview extends Disposable { return this._send('did-load-resource', { id, status: 404, - path: requestPath + path: uri.path, }); } diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index a5faa4d3316..6e7e3273000 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -511,7 +511,7 @@ export async function createWebviewManager(host) { * readonly allowMultipleAPIAcquire: boolean; * } * state: any; - * resourceEndpoint: string; + * cspSource: string; * }} ContentUpdateData */ @@ -553,10 +553,9 @@ export async function createWebviewManager(host) { } else { try { // Attempt to rewrite CSPs that hardcode old-style resource endpoint - const endpointUrl = new URL(data.resourceEndpoint); const cspContent = csp.getAttribute('content'); if (cspContent) { - const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); + const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, data.cspSource); csp.setAttribute('content', newCsp); } } catch (e) { diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 2589f552df0..6b1c64e7e1d 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -17,15 +17,11 @@ const rootPath = sw.location.pathname.replace(/\/service-worker.js$/, ''); const searchParams = new URL(location.toString()).searchParams; + /** * Origin used for resources */ -const resourceOrigin = searchParams.get('vscode-resource-origin') ?? sw.origin; - -/** - * Root path for resources - */ -const resourceRoot = '/vscode-resource'; +const resourceBaseAuthority = searchParams.get('vscode-resource-base-authority'); const serviceWorkerFetchIgnoreSubdomain = searchParams.get('serviceWorkerFetchIgnoreSubdomain') ?? false; @@ -175,17 +171,21 @@ sw.addEventListener('message', async (event) => { sw.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); - if (serviceWorkerFetchIgnoreSubdomain && requestUrl.pathname.startsWith(resourceRoot + '/')) { - // #121981 - const ignoreFirstSubdomainRegex = /(.*):\/\/.*?\.(.*)/; - const match1 = resourceOrigin.match(ignoreFirstSubdomainRegex); - const match2 = requestUrl.origin.match(ignoreFirstSubdomainRegex); - if (match1 && match2 && match1[1] === match2[1] && match1[2] === match2[2]) { + if (requestUrl.protocol === 'https:') { + if (serviceWorkerFetchIgnoreSubdomain) { + // #121981 + const requestHostParts = requestUrl.hostname.split('.'); // $scheme, vscode-resource, $uuid, $...authority + const baseAuthorityPaths = resourceBaseAuthority.split('.').slice(2); // $...authority + + // Make sure the all the parts except the uuid and scheme match + if (requestHostParts.length === 3 + baseAuthorityPaths.length) { + if (requestHostParts[1] === 'vscode-resource' && baseAuthorityPaths.every((part, i) => requestHostParts[3 + i] === part)) { + return event.respondWith(processResourceRequest(event, requestUrl)); + } + } + } else if (requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) { return event.respondWith(processResourceRequest(event, requestUrl)); } - } else if (requestUrl.origin === resourceOrigin && requestUrl.pathname.startsWith(resourceRoot + '/')) { - // See if it's a resource request - return event.respondWith(processResourceRequest(event, requestUrl)); } // See if it's a localhost request @@ -219,8 +219,6 @@ async function processResourceRequest(event, requestUrl) { return notFound(); } - const resourcePath = requestUrl.pathname.startsWith(resourceRoot + '/') ? requestUrl.pathname.slice(resourceRoot.length) : requestUrl.pathname; - /** * @param {ResourceResponse} entry * @param {Response | undefined} cachedResponse @@ -270,10 +268,16 @@ async function processResourceRequest(event, requestUrl) { const cached = await cache.match(event.request); const { requestId, promise } = resourceRequestStore.create(); + + const firstHostSegment = requestUrl.hostname.split('.')[0]; + const [_, scheme, authority] = firstHostSegment.match(/^(\w+)\+(.*)$/); + parentClient.postMessage({ channel: 'load-resource', id: requestId, - path: resourcePath, + path: requestUrl.pathname, + scheme, + authority: decodeURIComponent(authority), query: requestUrl.search.replace(/^\?/, ''), ifNoneMatch: cached?.headers.get('ETag'), }); diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 58b84ff93d4..90b99c5c67a 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -97,15 +97,22 @@ export class IFrameWebview extends BaseWebview implements Web this.element?.contentWindow?.focus(); } - protected initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions, extraParams?: object) { - const params = { + protected initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions, extraParams?: { [key: string]: string }) { + const params: { [key: string]: string } = { id: this.id, extensionId: extension?.id.value ?? '', // The extensionId and purpose in the URL are used for filtering in js-debug: - purpose: options.purpose, - serviceWorkerFetchIgnoreSubdomain: options.serviceWorkerFetchIgnoreSubdomain, ...extraParams, - 'vscode-resource-origin': this.webviewResourceOrigin, - } as const; + 'vscode-resource-base-authority': this.webviewRootResourceAuthority, + }; + + if (options.purpose) { + params.purpose = options.purpose; + } + + if (options.serviceWorkerFetchIgnoreSubdomain) { + params.serviceWorkerFetchIgnoreSubdomain = 'true'; + } + const queryString = (Object.keys(params) as Array) .map((key) => `${key}=${encodeURIComponent(params[key]!)}`) diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index fb055a5aedb..6e758d5dfae 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -159,7 +159,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme // and not the `vscode-file` URI because preload scripts are loaded // via node.js from the main side and only allow `file:` protocol this.element!.preload = FileAccess.asFileUri('./pre/electron-index.js', require).toString(true); - this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser-index.html?platform=electron&id=${this.id}&vscode-resource-origin=${encodeURIComponent(this.webviewResourceOrigin)}`; + this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser-index.html?platform=electron&id=${this.id}&vscode-resource-base-authority=${encodeURIComponent(this.webviewRootResourceAuthority)}`; } protected createElement(options: WebviewOptions) { diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index 12ebcc51e9a..c28af981081 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -15,7 +15,7 @@ import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDep import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; -import { webviewResourceAuthority } from 'vs/workbench/api/common/shared/webview'; +import { webviewResourceBaseHost } from 'vs/workbench/api/common/shared/webview'; import { EditorGroupColumn } from 'vs/workbench/common/editor'; import type * as vscode from 'vscode'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; @@ -80,32 +80,32 @@ suite('ExtHostWebview', () => { const webview = createWebview(rpcProtocol, /* remoteAuthority */undefined); assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), - `${webviewResourceAuthority}/vscode-resource/file//Users/codey/file.html`, + replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), + `https://file%2B.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix basic' ); assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), - `${webviewResourceAuthority}/vscode-resource/file//Users/codey/file.html#frag`, + replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), + `https://file%2B.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/file.html#frag`, 'Unix should preserve fragment' ); assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), - `${webviewResourceAuthority}/vscode-resource/file//Users/codey/f%20ile.html`, + replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), + `https://file%2B.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/f%20ile.html`, 'Unix with encoding' ); assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), - `${webviewResourceAuthority}/vscode-resource/file/localhost/Users/codey/file.html`, + replaceUuid(webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), + `https://file%2Blocalhost.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix should preserve authority' ); assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), - `${webviewResourceAuthority}/vscode-resource/file//c%3A/codey/file.txt`, + replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), + `https://file%2B.vscode-resource.UUID.${webviewResourceBaseHost}/c%3A/codey/file.txt`, 'Windows C drive' ); }); @@ -114,8 +114,8 @@ suite('ExtHostWebview', () => { const webview = createWebview(rpcProtocol, /* remoteAuthority */ 'remote'); assert.strictEqual( - stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), - `${webviewResourceAuthority}/vscode-resource/vscode-remote/remote/Users/codey/file.html`, + replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), + `https://vscode-remote%2Bremote.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix basic' ); }); @@ -141,8 +141,8 @@ function createWebview(rpcProtocol: (IExtHostRpcService & IExtHostContext) | und return webview; } -function stripEndpointUuid(input: string) { - return input.replace(/^https:\/\/[^\.]+?\./, ''); +function replaceUuid(input: string) { + return input.replace(/\.vscode-resource\.[^\.]+?\./, '.vscode-resource.UUID.'); } From 9847783f62dca76946c4f755860c7bb854222fd8 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 21 May 2021 09:28:28 -0700 Subject: [PATCH 265/270] Remove uuid from webview resource uris This is no longer be needed. We still serve the webview code itself from a unique subdomain This also removes the need for `serviceWorkerFetchIgnoreSubdomain` --- .../workbench/api/common/extHostCodeInsets.ts | 6 ++-- .../api/common/extHostNotebookKernels.ts | 2 +- src/vs/workbench/api/common/extHostWebview.ts | 2 +- src/vs/workbench/api/common/shared/webview.ts | 8 ++---- .../view/renderers/backLayerWebView.ts | 3 +- .../webview/browser/baseWebviewElement.ts | 6 ++-- .../webview/browser/pre/service-worker.js | 19 ++----------- .../contrib/webview/browser/webview.ts | 1 - .../contrib/webview/browser/webviewElement.ts | 5 ---- .../gettingStarted/browser/gettingStarted.ts | 2 +- .../test/browser/api/extHostWebview.test.ts | 28 ++++++++----------- 11 files changed, 26 insertions(+), 56 deletions(-) diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 291d6fbd4fc..fd7e9e715e5 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -8,10 +8,9 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; +import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; -import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { @@ -61,12 +60,11 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { const webview = new class implements vscode.Webview { - private readonly _uuid = generateUuid(); private _html: string = ''; private _options: vscode.WebviewOptions = Object.create(null); asWebviewUri(resource: vscode.Uri): vscode.Uri { - return asWebviewUri(this._uuid, resource, that._initData.remote); + return asWebviewUri(resource, that._initData.remote); } get cspSource(): string { diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index e342c86c04e..6eb1e1f6e53 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -190,7 +190,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); }, asWebviewUri(uri: URI) { - return asWebviewUri(String(handle), uri, that._initData.remote); + return asWebviewUri(uri, that._initData.remote); }, // --- priority updateNotebookAffinity(notebook, priority) { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 0aabbf99f3f..9de76732f60 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -69,7 +69,7 @@ export class ExtHostWebview implements vscode.Webview { public asWebviewUri(resource: vscode.Uri): vscode.Uri { this.#hasCalledAsWebviewUri = true; - return asWebviewUri(this.#handle, resource, this.#initData.remote); + return asWebviewUri(resource, this.#initData.remote); } public get cspSource(): string { diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index cf404c89bb7..e65663eace3 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -22,7 +22,7 @@ export interface WebviewInitData { */ export const webviewResourceBaseHost = 'vscode-webview.net'; -export const webviewRootResourceAuthority = (id: string) => `vscode-resource.${id}.${webviewResourceBaseHost}`; +export const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; @@ -33,15 +33,13 @@ export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; * we know where to load the resource from (remote or truly local): * * ```txt - * ${scheme}+${resource-authority}.vscode-resource.${id}.vscode-webview.net/${path} + * ${scheme}+${resource-authority}.vscode-resource.vscode-webview.net/${path} * ``` * - * @param uuid Unique id of the webview. * @param resource Uri of the resource to load. * @param remoteInfo Optional information about the remote that specifies where `resource` should be resolved from. */ export function asWebviewUri( - uuid: string, resource: vscode.Uri, remoteInfo?: { authority: string | undefined, isRemote: boolean } ): vscode.Uri { @@ -59,7 +57,7 @@ export function asWebviewUri( return URI.from({ scheme: Schemas.https, - authority: `${resource.scheme}+${encodeURIComponent(resource.authority)}.${webviewRootResourceAuthority(uuid)}`, + authority: `${resource.scheme}+${encodeURIComponent(resource.authority)}.${webviewRootResourceAuthority}`, path: resource.path, fragment: resource.fragment, query: resource.query, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 7b5afb08690..c00f3c1f24a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -787,7 +787,7 @@ export class BackLayerWebView extends Disposable { } private asWebviewUri(uri: URI, fromExtension: URI | undefined) { - return asWebviewUri(this.id, uri, fromExtension?.scheme === Schemas.vscodeRemote ? { isRemote: true, authority: fromExtension.authority } : undefined); + return asWebviewUri(uri, fromExtension?.scheme === Schemas.vscodeRemote ? { isRemote: true, authority: fromExtension.authority } : undefined); } postKernelMessage(message: any) { @@ -1188,7 +1188,6 @@ var requirejs = (function() { purpose: WebviewContentPurpose.NotebookRenderer, enableFindWidget: false, transformCssVariables: transformWebviewThemeVars, - serviceWorkerFetchIgnoreSubdomain: true }, { allowMultipleAPIAcquire: true, allowScripts: true, diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 0d04c6358b9..82de2459325 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -376,7 +376,7 @@ export abstract class BaseWebview extends Disposable { } protected get webviewRootResourceAuthority(): string { - return webviewRootResourceAuthority(this.id); + return webviewRootResourceAuthority; } private rewriteVsCodeResourceUrls(value: string): string { @@ -388,7 +388,7 @@ export abstract class BaseWebview extends Disposable { scheme: scheme || 'file', path: path, }); - const webviewUri = asWebviewUri(this.id, uri, { isRemote, authority: remoteAuthority }).toString(); + const webviewUri = asWebviewUri(uri, { isRemote, authority: remoteAuthority }).toString(); return `${startQuote}${webviewUri}${endQuote}`; }) .replace(/(["'])(?:vscode-webview-resource):(\/\/[^\s\/'"]+\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => { @@ -396,7 +396,7 @@ export abstract class BaseWebview extends Disposable { scheme: scheme || 'file', path: path, }); - const webviewUri = asWebviewUri(this.id, uri, { isRemote, authority: remoteAuthority }).toString(); + const webviewUri = asWebviewUri(uri, { isRemote, authority: remoteAuthority }).toString(); return `${startQuote}${webviewUri}${endQuote}`; }); } diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 6b1c64e7e1d..c109d848455 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -23,8 +23,6 @@ const searchParams = new URL(location.toString()).searchParams; */ const resourceBaseAuthority = searchParams.get('vscode-resource-base-authority'); -const serviceWorkerFetchIgnoreSubdomain = searchParams.get('serviceWorkerFetchIgnoreSubdomain') ?? false; - const resolveTimeout = 30000; /** @@ -171,21 +169,8 @@ sw.addEventListener('message', async (event) => { sw.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); - if (requestUrl.protocol === 'https:') { - if (serviceWorkerFetchIgnoreSubdomain) { - // #121981 - const requestHostParts = requestUrl.hostname.split('.'); // $scheme, vscode-resource, $uuid, $...authority - const baseAuthorityPaths = resourceBaseAuthority.split('.').slice(2); // $...authority - - // Make sure the all the parts except the uuid and scheme match - if (requestHostParts.length === 3 + baseAuthorityPaths.length) { - if (requestHostParts[1] === 'vscode-resource' && baseAuthorityPaths.every((part, i) => requestHostParts[3 + i] === part)) { - return event.respondWith(processResourceRequest(event, requestUrl)); - } - } - } else if (requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) { - return event.respondWith(processResourceRequest(event, requestUrl)); - } + if (requestUrl.protocol === 'https:' && requestUrl.hostname.endsWith('.' + resourceBaseAuthority)) { + return event.respondWith(processResourceRequest(event, requestUrl)); } // See if it's a localhost request diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 3dd09f3048f..822f4a312f0 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -80,7 +80,6 @@ export interface WebviewOptions { readonly enableFindWidget?: boolean; readonly tryRestoreScrollPosition?: boolean; readonly retainContextWhenHidden?: boolean; - readonly serviceWorkerFetchIgnoreSubdomain?: boolean; transformCssVariables?(styles: Readonly): Readonly; } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 90b99c5c67a..499d8db288a 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -109,11 +109,6 @@ export class IFrameWebview extends BaseWebview implements Web params.purpose = options.purpose; } - if (options.serviceWorkerFetchIgnoreSubdomain) { - params.serviceWorkerFetchIgnoreSubdomain = 'true'; - } - - const queryString = (Object.keys(params) as Array) .map((key) => `${key}=${encodeURIComponent(params[key]!)}`) .join('&'); diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts index 76d51764542..11fec69efca 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts @@ -492,7 +492,7 @@ export class GettingStartedPage extends EditorPane { if (src.startsWith('https://')) { return `src="${src}"`; } const path = joinPath(base, src); - const transformed = asWebviewUri(this.webviewID, path).toString(); + const transformed = asWebviewUri(path).toString(); return `src="${transformed}"`; }); diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index c28af981081..075e47d631a 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -80,32 +80,32 @@ suite('ExtHostWebview', () => { const webview = createWebview(rpcProtocol, /* remoteAuthority */undefined); assert.strictEqual( - replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), - `https://file%2B.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/file.html`, + (webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), + `https://file%2B.vscode-resource.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix basic' ); assert.strictEqual( - replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), - `https://file%2B.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/file.html#frag`, + (webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()), + `https://file%2B.vscode-resource.${webviewResourceBaseHost}/Users/codey/file.html#frag`, 'Unix should preserve fragment' ); assert.strictEqual( - replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), - `https://file%2B.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/f%20ile.html`, + (webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()), + `https://file%2B.vscode-resource.${webviewResourceBaseHost}/Users/codey/f%20ile.html`, 'Unix with encoding' ); assert.strictEqual( - replaceUuid(webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), - `https://file%2Blocalhost.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/file.html`, + (webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()), + `https://file%2Blocalhost.vscode-resource.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix should preserve authority' ); assert.strictEqual( - replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), - `https://file%2B.vscode-resource.UUID.${webviewResourceBaseHost}/c%3A/codey/file.txt`, + (webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()), + `https://file%2B.vscode-resource.${webviewResourceBaseHost}/c%3A/codey/file.txt`, 'Windows C drive' ); }); @@ -114,8 +114,8 @@ suite('ExtHostWebview', () => { const webview = createWebview(rpcProtocol, /* remoteAuthority */ 'remote'); assert.strictEqual( - replaceUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), - `https://vscode-remote%2Bremote.vscode-resource.UUID.${webviewResourceBaseHost}/Users/codey/file.html`, + (webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()), + `https://vscode-remote%2Bremote.vscode-resource.${webviewResourceBaseHost}/Users/codey/file.html`, 'Unix basic' ); }); @@ -141,10 +141,6 @@ function createWebview(rpcProtocol: (IExtHostRpcService & IExtHostContext) | und return webview; } -function replaceUuid(input: string) { - return input.replace(/\.vscode-resource\.[^\.]+?\./, '.vscode-resource.UUID.'); -} - function createNoopMainThreadWebviews() { return new class extends mock() { From a0d7f6292e96646c685a57896e97d07963dac773 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 21 May 2021 09:35:56 -0700 Subject: [PATCH 266/270] Remove a few instances of 'vs code' in d.ts For #124361 --- src/vs/vscode.d.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index a2321c80c67..6f3034930fa 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2061,7 +2061,7 @@ declare module 'vscode' { * * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`. * - * Code action kinds are used by VS Code for UI elements such as the refactoring context menu. Users + * Code action kinds are used by the editor for UI elements such as the refactoring context menu. Users * can also trigger code actions with a specific kind with the `editor.action.codeAction` command. */ export class CodeActionKind { @@ -2247,7 +2247,7 @@ declare module 'vscode' { /** * A {@link Command} this code action executes. * - * If this command throws an exception, VS Code displays the exception message to users in the editor at the + * If this command throws an exception, the editor displays the exception message to users in the editor at the * current cursor position. */ command?: Command; @@ -2278,7 +2278,7 @@ declare module 'vscode' { * of code action, such as refactorings. * * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) - * that auto applies a code action and only a disabled code actions are returned, VS Code will show the user an + * that auto applies a code action and only a disabled code actions are returned, the editor will show the user an * error message with `reason` in the editor. */ disabled?: { @@ -2362,10 +2362,10 @@ declare module 'vscode' { * * Documentation from the provider is shown in the code actions menu if either: * - * - Code actions of `kind` are requested by VS Code. In this case, VS Code will show the documentation that + * - Code actions of `kind` are requested by the editor. In this case, the editor will show the documentation that * most closely matches the requested code action kind. For example, if a provider has documentation for * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, - * VS Code will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. + * the editor will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. * * - Any code actions of `kind` are returned by the provider. * @@ -2384,7 +2384,7 @@ declare module 'vscode' { /** * Command that displays the documentation to the user. * - * This can display the documentation directly in VS Code or open a website using {@link env.openExternal `env.openExternal`}; + * This can display the documentation directly in the editor or open a website using {@link env.openExternal `env.openExternal`}; * * The title of this documentation code action is taken from {@link Command.title `Command.title`} */ @@ -2696,13 +2696,13 @@ declare module 'vscode' { /** * The evaluatable expression provider interface defines the contract between extensions and * the debug hover. In this contract the provider returns an evaluatable expression for a given position - * in a document and VS Code evaluates this expression in the active debug session and shows the result in a debug hover. + * in a document and the editor evaluates this expression in the active debug session and shows the result in a debug hover. */ export interface EvaluatableExpressionProvider { /** * Provide an evaluatable expression for the given document and position. - * VS Code will evaluate this expression in the active debug session and will show the result in the debug hover. + * The editor will evaluate this expression in the active debug session and will show the result in the debug hover. * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. * * @param document The document for which the debug hover is about to appear. @@ -8936,7 +8936,7 @@ declare module 'vscode' { * * Normally the webview's html context is created when the view becomes visible * and destroyed when it is hidden. Extensions that have complex state - * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview + * or UI can set the `retainContextWhenHidden` to make the editor keep the webview * context around, even when the webview moves to a background tab. When a webview using * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. * When the view becomes visible again, the context is automatically restored @@ -8953,7 +8953,7 @@ declare module 'vscode' { /** * Register a provider for custom editors for the `viewType` contributed by the `customEditors` extension point. * - * When a custom editor is opened, VS Code fires an `onCustomEditor:viewType` activation event. Your extension + * When a custom editor is opened, an `onCustomEditor:viewType` activation event is fired. Your extension * must register a {@link CustomTextEditorProvider `CustomTextEditorProvider`}, {@link CustomReadonlyEditorProvider `CustomReadonlyEditorProvider`}, * {@link CustomEditorProvider `CustomEditorProvider`}for `viewType` as part of activation. * @@ -8976,7 +8976,7 @@ declare module 'vscode' { * Indicates that the provider allows multiple editor instances to be open at the same time for * the same resource. * - * By default, VS Code only allows one editor instance to be open at a time for each resource. If the + * By default, the editor only allows one editor instance to be open at a time for each resource. If the * user tries to open a second editor instance for the resource, the first one is instead moved to where * the second one was to be opened. * @@ -10342,7 +10342,7 @@ declare module 'vscode' { export const rootPath: string | undefined; /** - * List of workspace folders that are open in VS Code. `undefined when no workspace + * List of workspace folders that are open in VS Code. `undefined` when no workspace * has been opened. * * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information From b78754e1e46b3010b9cae0921548a836a5e0f14d Mon Sep 17 00:00:00 2001 From: Mohammad Zainuddin <42219739+mzainuddin51@users.noreply.github.com> Date: Fri, 21 May 2021 22:32:47 +0530 Subject: [PATCH 267/270] fix incorrect links (#124203) Co-authored-by: Mohammad ZAINUDDIN --- src/vs/vscode.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 6f3034930fa..e02819b27f8 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -7782,7 +7782,7 @@ declare module 'vscode' { /** * Event triggered by extensions to signal to VS Code that an edit has occurred on an {@link CustomDocument `CustomDocument`}. * - * @see {@link CustomDocumentProvider.onDidChangeCustomDocument `CustomDocumentProvider.onDidChangeCustomDocument`}. + * @see {@link CustomEditorProvider.onDidChangeCustomDocument `CustomEditorProvider.onDidChangeCustomDocument`}. */ interface CustomDocumentEditEvent { @@ -7821,7 +7821,7 @@ declare module 'vscode' { * Event triggered by extensions to signal to VS Code that the content of a {@link CustomDocument `CustomDocument`} * has changed. * - * @see {@link CustomDocumentProvider.onDidChangeCustomDocument `CustomDocumentProvider.onDidChangeCustomDocument`}. + * @see {@link CustomEditorProvider.onDidChangeCustomDocument `CustomEditorProvider.onDidChangeCustomDocument`}. */ interface CustomDocumentContentChangeEvent { /** From c5da7f33a70addb3ae248fec15d7dadc8f1fb171 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 21 May 2021 10:16:21 -0700 Subject: [PATCH 268/270] Fix wsl detection in profiles tests Part of #124257 --- .../platform/terminal/node/terminalProfiles.ts | 2 +- .../test/node/terminalProfiles.test.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/terminal/node/terminalProfiles.ts b/src/vs/platform/terminal/node/terminalProfiles.ts index 7250ad21837..bdfae607ad6 100644 --- a/src/vs/platform/terminal/node/terminalProfiles.ts +++ b/src/vs/platform/terminal/node/terminalProfiles.ts @@ -34,7 +34,7 @@ export function detectAvailableProfiles( includeDetectedProfiles, fsProvider, logService, - safeConfigProvider(TerminalSettingId.UseWslProfiles) || true, + safeConfigProvider(TerminalSettingId.UseWslProfiles) !== false, safeConfigProvider(TerminalSettingId.ProfilesWindows), safeConfigProvider(TerminalSettingId.DefaultProfileWindows), variableResolver diff --git a/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts b/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts index 8e1eb26e79e..5e521205775 100644 --- a/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts +++ b/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts @@ -58,7 +58,7 @@ suite('Workbench - TerminalProfiles', () => { }, useWslProfiles: false }; - const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(config), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(config), fsProvider, undefined, undefined, undefined); const expected = [ { profileName: 'Git Bash', path: 'C:\\Program Files\\Git\\bin\\bash.exe', args: ['--login'], isDefault: true } ]; @@ -78,7 +78,7 @@ suite('Workbench - TerminalProfiles', () => { }, useWslProfiles: false }; - const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(config), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(config), fsProvider, undefined, undefined, undefined); const expected = [ { profileName: 'PowerShell', path: 'C:\\Program Files\\PowerShell\\7\\pwsh.exe', overrideName: true, args: ['-NoProfile'], isDefault: true } ]; @@ -98,7 +98,7 @@ suite('Workbench - TerminalProfiles', () => { }, useWslProfiles: false }; - const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(config), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(config), fsProvider, undefined, undefined, undefined); const expected = [{ profileName: 'Git Bash', path: 'C:\\Program Files\\Git\\bin\\bash.exe', args: [], isAutoDetected: undefined, overrideName: undefined, isDefault: true }]; profilesEqual(profiles, expected); }); @@ -120,7 +120,7 @@ suite('Workbench - TerminalProfiles', () => { 'C:\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe', 'C:\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' ]); - const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(pwshSourceConfig), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(pwshSourceConfig), fsProvider, undefined, undefined, undefined); const expected = [ { profileName: 'PowerShell', path: 'C:\\Program Files\\PowerShell\\7\\pwsh.exe', isDefault: true } ]; @@ -133,7 +133,7 @@ suite('Workbench - TerminalProfiles', () => { 'C:\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe', 'C:\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' ]); - const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(pwshSourceConfig), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(pwshSourceConfig), fsProvider, undefined, undefined, undefined); const expected = [ { profileName: 'PowerShell', path: 'C:\\Program Files\\PowerShell\\7\\pwsh.exe', isDefault: true } ]; @@ -144,7 +144,7 @@ suite('Workbench - TerminalProfiles', () => { 'C:\\Windows\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe', 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' ]); - const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(pwshSourceConfig), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(pwshSourceConfig), fsProvider, undefined, undefined, undefined); strictEqual(profiles.length, 1); strictEqual(profiles[0].profileName, 'PowerShell'); }); @@ -188,7 +188,7 @@ suite('Workbench - TerminalProfiles', () => { '/bin/fakeshell1', '/bin/fakeshell3' ]); - const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(absoluteConfig), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(absoluteConfig), fsProvider, undefined, undefined, undefined); const expected: ITerminalProfile[] = [ { profileName: 'fakeshell1', path: '/bin/fakeshell1', isDefault: true }, { profileName: 'fakeshell3', path: '/bin/fakeshell3', isDefault: true } @@ -200,7 +200,7 @@ suite('Workbench - TerminalProfiles', () => { '/bin/fakeshell1', '/bin/fakeshell3' ], '/bin/fakeshell1\n/bin/fakeshell3'); - const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(onPathConfig), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(onPathConfig), fsProvider, undefined, undefined, undefined); const expected: ITerminalProfile[] = [ { profileName: 'fakeshell1', path: 'fakeshell1', isDefault: true }, { profileName: 'fakeshell3', path: 'fakeshell3', isDefault: true } @@ -212,7 +212,7 @@ suite('Workbench - TerminalProfiles', () => { const fsProvider = createFsProvider([ '/bin/fakeshell1' ], '/bin/fakeshell1\n/bin/fakeshell3'); - const profiles = await detectAvailableProfiles(false, buildTestSafeConfigProvider(onPathConfig), fsProvider, undefined, undefined, undefined); + const profiles = await detectAvailableProfiles(true, buildTestSafeConfigProvider(onPathConfig), fsProvider, undefined, undefined, undefined); const expected: ITerminalProfile[] = [ { profileName: 'fakeshell1', path: 'fakeshell1', isDefault: true } ]; From 852b70eafedd9ecf4a6b2587d14845a8ee91c214 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Fri, 21 May 2021 10:36:20 -0700 Subject: [PATCH 269/270] Try fix (#124384) --- test/automation/src/search.ts | 2 +- test/smoke/src/areas/search/search.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/automation/src/search.ts b/test/automation/src/search.ts index d5637906834..247c714ab6b 100644 --- a/test/automation/src/search.ts +++ b/test/automation/src/search.ts @@ -44,7 +44,7 @@ export class Search extends Viewlet { } async getSearchTooltip(): Promise { - const icon = await this.code.waitForElement(`.activitybar .action-label.codicon.codicon-search-view-icon`); + const icon = await this.code.waitForElement(`.activitybar .action-label.codicon.codicon-search-view-icon`, (el) => !!el?.attributes?.['title']); return icon.attributes['title']; } diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 4eca396976e..72ec498f988 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -16,7 +16,7 @@ export function setup() { }); // https://github.com/microsoft/vscode/issues/124146 - it.skip /* https://github.com/microsoft/vscode/issues/124335 */('has a tooltp with a keybinding', async function () { + it('has a tooltp with a keybinding', async function () { const app = this.app as Application; const tooltip: string = await app.workbench.search.getSearchTooltip(); if (!/Search \(.+\)/.test(tooltip)) { From 8b709a2f55aa10b43c165e02fe7827a5e8cbde26 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 21 May 2021 10:44:32 -0700 Subject: [PATCH 270/270] testing: provide test IDs in menu contributions, rather than internal elements --- .../testing/browser/testExplorerActions.ts | 126 +++++++++++++----- .../testing/browser/testing.contribution.ts | 32 +---- .../testing/browser/testingExplorerView.ts | 11 +- 3 files changed, 105 insertions(+), 64 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 6cba69714ed..75ed69346af 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -60,10 +60,10 @@ const enum ActionOrder { } export class HideTestAction extends Action2 { - constructor( - ) { + public static readonly ID = 'testing.hideTest'; + constructor() { super({ - id: 'testing.hideTest', + id: HideTestAction.ID, title: localize('hideTest', 'Hide Test'), f1: false, menu: { @@ -85,9 +85,10 @@ export class HideTestAction extends Action2 { } export class UnhideTestAction extends Action2 { + public static readonly ID = 'testing.unhideTest'; constructor() { super({ - id: 'testing.unhideTest', + id: UnhideTestAction.ID, title: localize('unhideTest', 'Unhide Test'), f1: false, menu: { @@ -109,9 +110,10 @@ export class UnhideTestAction extends Action2 { } export class DebugAction extends Action2 { + public static readonly ID = 'testing.debug'; constructor() { super({ - id: 'testing.debug', + id: DebugAction.ID, title: localize('debug test', 'Debug Test'), icon: icons.testingDebugIcon, f1: false, @@ -134,9 +136,10 @@ export class DebugAction extends Action2 { export class RunAction extends Action2 { + public static readonly ID = 'testing.run'; constructor() { super({ - id: 'testing.run', + id: RunAction.ID, title: localize('run test', 'Run Test'), icon: icons.testingRunIcon, f1: false, @@ -211,10 +214,11 @@ abstract class RunOrDebugSelectedAction extends ViewAction } export class RunSelectedAction extends RunOrDebugSelectedAction { - constructor( - ) { + public static readonly ID = 'testing.runSelected'; + + constructor() { super( - 'testing.runSelected', + RunSelectedAction.ID, localize('runSelectedTests', 'Run Selected Tests'), icons.testingRunIcon, false, @@ -230,9 +234,10 @@ export class RunSelectedAction extends RunOrDebugSelectedAction { } export class DebugSelectedAction extends RunOrDebugSelectedAction { + public static readonly ID = 'testing.debugSelected'; constructor() { super( - 'testing.debugSelected', + DebugSelectedAction.ID, localize('debugSelectedTests', 'Debug Selected Tests'), icons.testingDebugIcon, true, @@ -314,9 +319,10 @@ abstract class RunOrDebugAllAllAction extends Action2 { } export class RunAllAction extends RunOrDebugAllAllAction { + public static readonly ID = 'testing.runAll'; constructor() { super( - 'testing.runAll', + RunAllAction.ID, localize('runAllTests', 'Run All Tests'), icons.testingRunAllIcon, false, @@ -326,9 +332,10 @@ export class RunAllAction extends RunOrDebugAllAllAction { } export class DebugAllAction extends RunOrDebugAllAllAction { + public static readonly ID = 'testing.debugAll'; constructor() { super( - 'testing.debugAll', + DebugAllAction.ID, localize('debugAllTests', 'Debug All Tests'), icons.testingDebugIcon, true, @@ -338,9 +345,10 @@ export class DebugAllAction extends RunOrDebugAllAllAction { } export class CancelTestRunAction extends Action2 { + public static readonly ID = 'testing.cancelRun'; constructor() { super({ - id: 'testing.cancelRun', + id: CancelTestRunAction.ID, title: localize('testing.cancelRun', "Cancel Test Run"), icon: icons.testingCancelIcon, menu: { @@ -367,9 +375,10 @@ export class CancelTestRunAction extends Action2 { } export class TestingViewAsListAction extends ViewAction { + public static readonly ID = 'testing.viewAsList'; constructor() { super({ - id: 'testing.viewAsList', + id: TestingViewAsListAction.ID, viewId: Testing.ExplorerViewId, title: localize('testing.viewAsList', "View as List"), f1: false, @@ -392,9 +401,10 @@ export class TestingViewAsListAction extends ViewAction { } export class TestingViewAsTreeAction extends ViewAction { + public static readonly ID = 'testing.viewAsTree'; constructor() { super({ - id: 'testing.viewAsTree', + id: TestingViewAsTreeAction.ID, viewId: Testing.ExplorerViewId, title: localize('testing.viewAsTree', "View as Tree"), f1: false, @@ -418,9 +428,10 @@ export class TestingViewAsTreeAction extends ViewAction { export class TestingSortByNameAction extends ViewAction { + public static readonly ID = 'testing.sortByName'; constructor() { super({ - id: 'testing.sortByName', + id: TestingSortByNameAction.ID, viewId: Testing.ExplorerViewId, title: localize('testing.sortByName', "Sort by Name"), f1: false, @@ -443,9 +454,10 @@ export class TestingSortByNameAction extends ViewAction { } export class TestingSortByLocationAction extends ViewAction { + public static readonly ID = 'testing.sortByLocation'; constructor() { super({ - id: 'testing.sortByLocation', + id: TestingSortByLocationAction.ID, viewId: Testing.ExplorerViewId, title: localize('testing.sortByLocation', "Sort by Location"), f1: false, @@ -468,9 +480,10 @@ export class TestingSortByLocationAction extends ViewAction } export class ShowMostRecentOutputAction extends Action2 { + public static readonly ID = 'testing.showMostRecentOutput'; constructor() { super({ - id: 'testing.showMostRecentOutput', + id: ShowMostRecentOutputAction.ID, title: localize('testing.showMostRecentOutput', "Show Output"), f1: true, category, @@ -492,9 +505,10 @@ export class ShowMostRecentOutputAction extends Action2 { export class CollapseAllAction extends ViewAction { + public static readonly ID = 'testing.collapseAll'; constructor() { super({ - id: 'testing.collapseAll', + id: CollapseAllAction.ID, viewId: Testing.ExplorerViewId, title: localize('testing.collapseAll', "Collapse All Tests"), f1: false, @@ -517,9 +531,10 @@ export class CollapseAllAction extends ViewAction { } export class RefreshTestsAction extends Action2 { + public static readonly ID = 'testing.refreshTests'; constructor() { super({ - id: 'testing.refreshTests', + id: RefreshTestsAction.ID, title: localize('testing.refresh', "Refresh Tests"), category, f1: true, @@ -541,9 +556,10 @@ export class RefreshTestsAction extends Action2 { } export class ClearTestResultsAction extends Action2 { + public static readonly ID = 'testing.clearTestResults'; constructor() { super({ - id: 'testing.clearTestResults', + id: ClearTestResultsAction.ID, title: localize('testing.clearResults', "Clear All Results"), category, f1: true @@ -559,9 +575,10 @@ export class ClearTestResultsAction extends Action2 { } export class GoToTest extends Action2 { + public static readonly ID = 'testing.editFocusedTest'; constructor() { super({ - id: 'testing.editFocusedTest', + id: GoToTest.ID, title: localize('testing.editFocusedTest', "Go to Test"), f1: false, menu: { @@ -676,9 +693,11 @@ export class GoToTest extends Action2 { } abstract class ToggleAutoRun extends Action2 { + public static readonly ID = 'testing.toggleautoRun'; + constructor(title: string, whenToggleIs: boolean) { super({ - id: 'testing.toggleautoRun', + id: ToggleAutoRun.ID, title, f1: true, icon: icons.testingAutorunIcon, @@ -767,9 +786,10 @@ abstract class RunOrDebugAtCursor extends Action2 { } export class RunAtCursor extends RunOrDebugAtCursor { + public static readonly ID = 'testing.runAtCursor'; constructor() { super({ - id: 'testing.runAtCursor', + id: RunAtCursor.ID, title: localize('testing.runAtCursor', "Run Test at Cursor"), f1: true, category, @@ -789,9 +809,10 @@ export class RunAtCursor extends RunOrDebugAtCursor { } export class DebugAtCursor extends RunOrDebugAtCursor { + public static readonly ID = 'testing.debugAtCursor'; constructor() { super({ - id: 'testing.debugAtCursor', + id: DebugAtCursor.ID, title: localize('testing.debugAtCursor', "Debug Test at Cursor"), f1: true, category, @@ -847,9 +868,10 @@ abstract class RunOrDebugCurrentFile extends Action2 { } export class RunCurrentFile extends RunOrDebugCurrentFile { + public static readonly ID = 'testing.runCurrentFile'; constructor() { super({ - id: 'testing.runCurrentFile', + id: RunCurrentFile.ID, title: localize('testing.runCurrentFile', "Run Tests in Current File"), f1: true, category, @@ -869,9 +891,10 @@ export class RunCurrentFile extends RunOrDebugCurrentFile { } export class DebugCurrentFile extends RunOrDebugCurrentFile { + public static readonly ID = 'testing.debugCurrentFile'; constructor() { super({ - id: 'testing.debugCurrentFile', + id: DebugCurrentFile.ID, title: localize('testing.debugCurrentFile', "Debug Tests in Current File"), f1: true, category, @@ -967,9 +990,10 @@ abstract class RunOrDebugLastRun extends RunOrDebugExtsById { } export class ReRunFailedTests extends RunOrDebugFailedTests { + public static readonly ID = 'testing.reRunFailTests'; constructor() { super({ - id: 'testing.reRunFailTests', + id: ReRunFailedTests.ID, title: localize('testing.reRunFailTests', "Rerun Failed Tests"), f1: true, category, @@ -989,9 +1013,10 @@ export class ReRunFailedTests extends RunOrDebugFailedTests { } export class DebugFailedTests extends RunOrDebugFailedTests { + public static readonly ID = 'testing.debugFailTests'; constructor() { super({ - id: 'testing.debugFailTests', + id: DebugFailedTests.ID, title: localize('testing.debugFailTests', "Debug Failed Tests"), f1: true, category, @@ -1011,9 +1036,10 @@ export class DebugFailedTests extends RunOrDebugFailedTests { } export class ReRunLastRun extends RunOrDebugLastRun { + public static readonly ID = 'testing.reRunLastRun'; constructor() { super({ - id: 'testing.reRunLastRun', + id: ReRunLastRun.ID, title: localize('testing.reRunLastRun', "Rerun Last Run"), f1: true, category, @@ -1033,9 +1059,10 @@ export class ReRunLastRun extends RunOrDebugLastRun { } export class DebugLastRun extends RunOrDebugLastRun { + public static readonly ID = 'testing.debugLastRun'; constructor() { super({ - id: 'testing.debugLastRun', + id: DebugLastRun.ID, title: localize('testing.debugLastRun', "Debug Last Run"), f1: true, category, @@ -1055,9 +1082,10 @@ export class DebugLastRun extends RunOrDebugLastRun { } export class SearchForTestExtension extends Action2 { + public static readonly ID = 'testing.searchForTestExtension'; constructor() { super({ - id: 'testing.searchForTestExtension', + id: SearchForTestExtension.ID, title: localize('testing.searchForTestExtension', "Search for Test Extension"), f1: false, }); @@ -1070,3 +1098,37 @@ export class SearchForTestExtension extends Action2 { viewlet.focus(); } } + +export const allTestActions = [ + AutoRunOffAction, + AutoRunOnAction, + CancelTestRunAction, + ClearTestResultsAction, + CollapseAllAction, + DebugAction, + DebugAllAction, + DebugAtCursor, + DebugCurrentFile, + DebugFailedTests, + DebugLastRun, + DebugSelectedAction, + GoToTest, + HideTestAction, + RefreshTestsAction, + ReRunFailedTests, + ReRunLastRun, + RunAction, + RunAllAction, + RunAtCursor, + RunCurrentFile, + RunSelectedAction, + SearchForTestExtension, + ShowMostRecentOutputAction, + TestingSortByLocationAction, + TestingSortByNameAction, + TestingViewAsListAction, + TestingViewAsTreeAction, + UnhideTestAction, +]; + +export const internalTestActionIds = new Set(allTestActions.map(a => a.ID)); diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index a3587ce2d74..15721c66065 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -35,7 +35,7 @@ import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl'; import { IWorkspaceTestCollectionService, WorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import * as Action from './testExplorerActions'; +import { allTestActions } from './testExplorerActions'; registerSingleton(ITestService, TestService); registerSingleton(ITestResultStorage, TestResultStorage); @@ -89,35 +89,7 @@ viewsRegistry.registerViews([{ when: ContextKeyExpr.greater(TestingContextKeys.providerCount.key, 0), }], viewContainer); -registerAction2(Action.AutoRunOffAction); -registerAction2(Action.AutoRunOnAction); -registerAction2(Action.CancelTestRunAction); -registerAction2(Action.ClearTestResultsAction); -registerAction2(Action.CollapseAllAction); -registerAction2(Action.DebugAction); -registerAction2(Action.DebugAllAction); -registerAction2(Action.DebugAtCursor); -registerAction2(Action.DebugCurrentFile); -registerAction2(Action.DebugFailedTests); -registerAction2(Action.DebugLastRun); -registerAction2(Action.DebugSelectedAction); -registerAction2(Action.GoToTest); -registerAction2(Action.HideTestAction); -registerAction2(Action.RefreshTestsAction); -registerAction2(Action.ReRunFailedTests); -registerAction2(Action.ReRunLastRun); -registerAction2(Action.RunAction); -registerAction2(Action.RunAllAction); -registerAction2(Action.RunAtCursor); -registerAction2(Action.RunCurrentFile); -registerAction2(Action.RunSelectedAction); -registerAction2(Action.SearchForTestExtension); -registerAction2(Action.ShowMostRecentOutputAction); -registerAction2(Action.TestingSortByLocationAction); -registerAction2(Action.TestingSortByNameAction); -registerAction2(Action.TestingViewAsListAction); -registerAction2(Action.TestingViewAsTreeAction); -registerAction2(Action.UnhideTestAction); +allTestActions.forEach(registerAction2); registerAction2(CloseTestPeek); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 05fa88cabb3..8d7fff00645 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -63,7 +63,7 @@ import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResu import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IWorkspaceTestCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { GoToTest } from './testExplorerActions'; +import { GoToTest, internalTestActionIds } from './testExplorerActions'; export class TestingExplorerView extends ViewPane { public viewModel!: TestingExplorerViewModel; @@ -811,7 +811,14 @@ class TestExplorerActionRunner extends ActionRunner { const selection = this.getSelectedTests(); const contextIsSelected = selection.some(s => s === context); const actualContext = contextIsSelected ? selection : [context]; - await action.run(...actualContext.filter(isActionableTestTreeElement)); + const actionable = actualContext.filter(isActionableTestTreeElement); + + // Is there a better way to do this? + if (internalTestActionIds.has(action.id)) { + await action.run(...actionable); + } else { + await action.run(...actionable.map(a => a instanceof TestItemTreeElement ? a.test.item.extId : a.folder.uri)); + } } }