diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 476ee9137a1..abc6ba1f4a2 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -6,7 +6,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: versionSpec: "1.x" @@ -18,7 +18,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - script: | yarn electron x64 diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index a87e3753e77..668194b66f2 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -14,7 +14,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: versionSpec: "1.x" @@ -26,7 +26,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - script: | yarn electron x64 diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 7e16a00f0d8..9351bafa0bd 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -13,7 +13,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' - powershell: | yarn --frozen-lockfile env: @@ -24,7 +24,7 @@ steps: inputs: keyfile: '.yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' + vstsFeed: 'vscode-build-cache' condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - powershell: | yarn electron diff --git a/extensions/image-preview/media/main.js b/extensions/image-preview/media/main.js index bc1e25a4bdf..3e2abf4ee98 100644 --- a/extensions/image-preview/media/main.js +++ b/extensions/image-preview/media/main.js @@ -303,7 +303,7 @@ document.body.classList.remove('loading'); }); - image.src = decodeURI(settings.src); + image.src = settings.src; window.addEventListener('message', e => { switch (e.data.type) { diff --git a/extensions/image-preview/src/binarySizeStatusBarEntry.ts b/extensions/image-preview/src/binarySizeStatusBarEntry.ts new file mode 100644 index 00000000000..1f48580d1e7 --- /dev/null +++ b/extensions/image-preview/src/binarySizeStatusBarEntry.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 * as vscode from 'vscode'; +import { Disposable } from './dispose'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +class BinarySize { + static readonly KB = 1024; + static readonly MB = BinarySize.KB * BinarySize.KB; + static readonly GB = BinarySize.MB * BinarySize.KB; + static readonly TB = BinarySize.GB * BinarySize.KB; + + static formatSize(size: number): string { + if (size < BinarySize.KB) { + return localize('sizeB', "{0}B", size); + } + + if (size < BinarySize.MB) { + return localize('sizeKB', "{0}KB", (size / BinarySize.KB).toFixed(2)); + } + + if (size < BinarySize.GB) { + return localize('sizeMB', "{0}MB", (size / BinarySize.MB).toFixed(2)); + } + + if (size < BinarySize.TB) { + return localize('sizeGB', "{0}GB", (size / BinarySize.GB).toFixed(2)); + } + + return localize('sizeTB', "{0}TB", (size / BinarySize.TB).toFixed(2)); + } +} + +export class BinarySizeStatusBarEntry extends Disposable { + private readonly _entry: vscode.StatusBarItem; + + private _showingOwner: string | undefined; + + constructor() { + super(); + this._entry = this._register(vscode.window.createStatusBarItem({ + id: 'imagePreview.binarySize', + name: localize('sizeStatusBar.name', "Image Binary Size"), + alignment: vscode.StatusBarAlignment.Right, + priority: 100, + })); + } + + public show(owner: string, size: number | undefined) { + this._showingOwner = owner; + if (typeof size === 'number') { + this._entry.text = BinarySize.formatSize(size); + this._entry.show(); + } else { + this.hide(owner); + } + } + + public hide(owner: string) { + if (owner === this._showingOwner) { + this._entry.hide(); + this._showingOwner = undefined; + } + } +} diff --git a/extensions/image-preview/src/extension.ts b/extensions/image-preview/src/extension.ts index 49563c75ee5..d1079ccdfa6 100644 --- a/extensions/image-preview/src/extension.ts +++ b/extensions/image-preview/src/extension.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import { PreviewManager } from './preview'; import { SizeStatusBarEntry } from './sizeStatusBarEntry'; +import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; import { ZoomStatusBarEntry } from './zoomStatusBarEntry'; export function activate(context: vscode.ExtensionContext) { @@ -14,10 +15,13 @@ export function activate(context: vscode.ExtensionContext) { const sizeStatusBarEntry = new SizeStatusBarEntry(); context.subscriptions.push(sizeStatusBarEntry); + const binarySizeStatusBarEntry = new BinarySizeStatusBarEntry(); + context.subscriptions.push(binarySizeStatusBarEntry); + const zoomStatusBarEntry = new ZoomStatusBarEntry(); context.subscriptions.push(zoomStatusBarEntry); - const previewManager = new PreviewManager(extensionRoot, sizeStatusBarEntry, zoomStatusBarEntry); + const previewManager = new PreviewManager(extensionRoot, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry); context.subscriptions.push(vscode.window.registerWebviewEditorProvider( PreviewManager.viewType, diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index 8aa6d1fe395..79e49832e41 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -8,6 +8,7 @@ import * as nls from 'vscode-nls'; import { Disposable } from './dispose'; import { SizeStatusBarEntry } from './sizeStatusBarEntry'; import { Scale, ZoomStatusBarEntry } from './zoomStatusBarEntry'; +import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; const localize = nls.loadMessageBundle(); @@ -22,6 +23,7 @@ export class PreviewManager { constructor( private readonly extensionRoot: vscode.Uri, private readonly sizeStatusBarEntry: SizeStatusBarEntry, + private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry, private readonly zoomStatusBarEntry: ZoomStatusBarEntry, ) { } @@ -29,7 +31,7 @@ export class PreviewManager { resource: vscode.Uri, webviewEditor: vscode.WebviewPanel, ): vscode.WebviewEditorCapabilities { - const preview = new Preview(this.extensionRoot, resource, webviewEditor, this.sizeStatusBarEntry, this.zoomStatusBarEntry); + const preview = new Preview(this.extensionRoot, resource, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry); this._previews.add(preview); this.setActivePreview(preview); @@ -72,6 +74,7 @@ class Preview extends Disposable implements vscode.WebviewEditorEditingCapabilit private _previewState = PreviewState.Visible; private _imageSize: string | undefined; + private _imageBinarySize: number | undefined; private _imageZoom: Scale | undefined; constructor( @@ -79,6 +82,7 @@ class Preview extends Disposable implements vscode.WebviewEditorEditingCapabilit private readonly resource: vscode.Uri, private readonly webviewEditor: vscode.WebviewPanel, private readonly sizeStatusBarEntry: SizeStatusBarEntry, + private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry, private readonly zoomStatusBarEntry: ZoomStatusBarEntry, ) { super(); @@ -125,6 +129,7 @@ class Preview extends Disposable implements vscode.WebviewEditorEditingCapabilit this._register(webviewEditor.onDidDispose(() => { if (this._previewState === PreviewState.Active) { this.sizeStatusBarEntry.hide(this.id); + this.binarySizeStatusBarEntry.hide(this.id); this.zoomStatusBarEntry.hide(this.id); } this._previewState = PreviewState.Disposed; @@ -142,6 +147,11 @@ class Preview extends Disposable implements vscode.WebviewEditorEditingCapabilit } })); + vscode.workspace.fs.stat(resource).then(({ size }) => { + this._imageBinarySize = size; + this.update(); + }); + this.render(); this.update(); this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active }); @@ -173,10 +183,12 @@ class Preview extends Disposable implements vscode.WebviewEditorEditingCapabilit if (this.webviewEditor.active) { this._previewState = PreviewState.Active; this.sizeStatusBarEntry.show(this.id, this._imageSize || ''); + this.binarySizeStatusBarEntry.show(this.id, this._imageBinarySize); this.zoomStatusBarEntry.show(this.id, this._imageZoom || 'fit'); } else { if (this._previewState === PreviewState.Active) { this.sizeStatusBarEntry.hide(this.id); + this.binarySizeStatusBarEntry.hide(this.id); this.zoomStatusBarEntry.hide(this.id); } this._previewState = PreviewState.Visible; @@ -219,18 +231,18 @@ class Preview extends Disposable implements vscode.WebviewEditorEditingCapabilit private getResourcePath(webviewEditor: vscode.WebviewPanel, resource: vscode.Uri, version: string) { switch (resource.scheme) { case 'data': - return encodeURI(resource.toString(true)); + return resource.toString(true); case 'git': // Show blank image - return encodeURI('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg=='); + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg=='; default: // Avoid adding cache busting if there is already a query string if (resource.query) { - return encodeURI(webviewEditor.webview.asWebviewUri(resource).toString(true)); + return encodeURI(webviewEditor.webview.asWebviewUri(resource).toString()); } - return encodeURI(webviewEditor.webview.asWebviewUri(resource).toString(true) + `?version=${version}`); + return encodeURI(webviewEditor.webview.asWebviewUri(resource).toString() + `?version=${version}`); } } @@ -248,10 +260,10 @@ class Preview extends Disposable implements vscode.WebviewEditorEditingCapabilit async hotExit() { } - async applyEdits(_edits: any[]) { } - async undoEdits(edits: any[]) { console.log('undo', edits); } + async applyEdits(edits: any[]) { console.log('apply', edits); } + //#endregion public test_makeEdit() { diff --git a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts b/extensions/typescript-language-features/src/features/bufferSyncSupport.ts index 3cb7f24d80a..43775c78ca0 100644 --- a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/features/bufferSyncSupport.ts @@ -36,27 +36,31 @@ function mode2ScriptKind(mode: string): 'TS' | 'TSX' | 'JS' | 'JSX' | undefined return undefined; } +const enum BufferOperationType { Close, Open, Change } + class CloseOperation { - readonly type = 'close'; + readonly type = BufferOperationType.Close; constructor( public readonly args: string ) { } } class OpenOperation { - readonly type = 'open'; + readonly type = BufferOperationType.Open; constructor( public readonly args: Proto.OpenRequestArgs ) { } } class ChangeOperation { - readonly type = 'change'; + readonly type = BufferOperationType.Change; constructor( public readonly args: Proto.FileCodeEdits ) { } } +type BufferOperation = CloseOperation | OpenOperation | ChangeOperation; + /** * Manages synchronization of buffers with the TS server. * @@ -64,7 +68,7 @@ class ChangeOperation { */ class BufferSynchronizer { - private readonly _pending = new ResourceMap(); + private readonly _pending = new ResourceMap(); constructor( private readonly client: ITypeScriptServiceClient @@ -72,9 +76,7 @@ class BufferSynchronizer { public open(resource: vscode.Uri, args: Proto.OpenRequestArgs) { if (this.supportsBatching) { - this.updatePending(resource, pending => { - pending.set(resource, new OpenOperation(args)); - }); + this.updatePending(resource, new OpenOperation(args)); } else { this.client.executeWithoutWaitingForResponse('open', args); } @@ -82,9 +84,7 @@ class BufferSynchronizer { public close(resource: vscode.Uri, filepath: string) { if (this.supportsBatching) { - this.updatePending(resource, pending => { - pending.set(resource, new CloseOperation(filepath)); - }); + this.updatePending(resource, new CloseOperation(filepath)); } else { const args: Proto.FileRequestArgs = { file: filepath }; this.client.executeWithoutWaitingForResponse('close', args); @@ -97,16 +97,14 @@ class BufferSynchronizer { } if (this.supportsBatching) { - this.updatePending(resource, pending => { - pending.set(resource, new ChangeOperation({ - fileName: filepath, - textChanges: events.map((change): Proto.CodeEdit => ({ - newText: change.text, - start: typeConverters.Position.toLocation(change.range.start), - end: typeConverters.Position.toLocation(change.range.end), - })).reverse(), // Send the edits end-of-document to start-of-document order - })); - }); + this.updatePending(resource, new ChangeOperation({ + fileName: filepath, + textChanges: events.map((change): Proto.CodeEdit => ({ + newText: change.text, + start: typeConverters.Position.toLocation(change.range.start), + end: typeConverters.Position.toLocation(change.range.end), + })).reverse(), // Send the edits end-of-document to start-of-document order + })); } else { for (const { range, text } of events) { const args: Proto.ChangeRequestArgs = { @@ -143,9 +141,9 @@ class BufferSynchronizer { const changedFiles: Proto.FileCodeEdits[] = []; for (const change of this._pending.values) { switch (change.type) { - case 'change': changedFiles.push(change.args); break; - case 'open': openFiles.push(change.args); break; - case 'close': closedFiles.push(change.args); break; + case BufferOperationType.Change: changedFiles.push(change.args); break; + case BufferOperationType.Open: openFiles.push(change.args); break; + case BufferOperationType.Close: closedFiles.push(change.args); break; } } this.client.execute('updateOpen', { changedFiles, closedFiles, openFiles }, nulToken, { nonRecoverable: true }); @@ -157,12 +155,23 @@ class BufferSynchronizer { return this.client.apiVersion.gte(API.v340); } - private updatePending(resource: vscode.Uri, f: (pending: ResourceMap) => void): void { + private updatePending(resource: vscode.Uri, op: BufferOperation): void { + switch (op.type) { + case BufferOperationType.Close: + const existing = this._pending.get(resource); + switch (existing?.type) { + case BufferOperationType.Open: + this._pending.delete(resource); + return; // Open then close. No need to do anything + } + break; + } + if (this._pending.has(resource)) { // we saw this file before, make sure we flush before working with it again this.flush(); } - f(this._pending); + this._pending.set(resource, op); } } diff --git a/package.json b/package.json index 10caabb82ad..ac7ab9e99e9 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "xterm": "4.3.0-beta17", "xterm-addon-search": "0.4.0-beta4", "xterm-addon-web-links": "0.2.1", + "xterm-addon-webgl": "0.4.0-beta6", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/package.json b/remote/package.json index 7b87df3defd..1a1c1a0e64a 100644 --- a/remote/package.json +++ b/remote/package.json @@ -23,6 +23,7 @@ "xterm": "4.3.0-beta17", "xterm-addon-search": "0.4.0-beta4", "xterm-addon-web-links": "0.2.1", + "xterm-addon-webgl": "0.4.0-beta6", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 9fcdaa00992..c4a39c50bb3 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,6 +7,7 @@ "vscode-textmate": "^4.3.0", "xterm": "4.3.0-beta17", "xterm-addon-search": "0.4.0-beta4", - "xterm-addon-web-links": "0.2.1" + "xterm-addon-web-links": "0.2.1", + "xterm-addon-webgl": "0.4.0-beta6" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 1ecb4b7cb52..d5d1fcdd499 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -41,6 +41,11 @@ xterm-addon-web-links@0.2.1: resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.2.1.tgz#6d1f2ce613e09870badf17615e7a1170a31542b2" integrity sha512-2KnHtiq0IG7hfwv3jw2/jQeH1RBk2d5CH4zvgwQe00rLofSJqSfgnJ7gwowxxpGHrpbPr6Lv4AmH/joaNw2+HQ== +xterm-addon-webgl@0.4.0-beta6: + version "0.4.0-beta6" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta6.tgz#23d152a0467d8b1f96ab3da7ac9a49bfa0b08c98" + integrity sha512-gtM8XtRyrNFCtxHBIU3pqTlBeAi5VOoymJAXKQQ7RsHEVJX79OTk1dQ9Q6Ow14+REGwQU/zFECV050jbLTfvrQ== + xterm@4.3.0-beta17: version "4.3.0-beta17" resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta17.tgz#c038cc00cb5be33d2a5f083255c329d9ed186565" diff --git a/remote/yarn.lock b/remote/yarn.lock index 8f2882c9714..c2f8da25470 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -428,6 +428,11 @@ xterm-addon-web-links@0.2.1: resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.2.1.tgz#6d1f2ce613e09870badf17615e7a1170a31542b2" integrity sha512-2KnHtiq0IG7hfwv3jw2/jQeH1RBk2d5CH4zvgwQe00rLofSJqSfgnJ7gwowxxpGHrpbPr6Lv4AmH/joaNw2+HQ== +xterm-addon-webgl@0.4.0-beta6: + version "0.4.0-beta6" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta6.tgz#23d152a0467d8b1f96ab3da7ac9a49bfa0b08c98" + integrity sha512-gtM8XtRyrNFCtxHBIU3pqTlBeAi5VOoymJAXKQQ7RsHEVJX79OTk1dQ9Q6Ow14+REGwQU/zFECV050jbLTfvrQ== + xterm@4.3.0-beta17: version "4.3.0-beta17" resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta17.tgz#c038cc00cb5be33d2a5f083255c329d9ed186565" diff --git a/src/vs/base/browser/canIUse.ts b/src/vs/base/browser/canIUse.ts index a8fc3db9d80..9329acfad6a 100644 --- a/src/vs/base/browser/canIUse.ts +++ b/src/vs/base/browser/canIUse.ts @@ -56,5 +56,5 @@ export const BrowserFeatures = { })(), touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0, - pointerEvents: browser.isSafari && window.PointerEvent && ('ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0) + pointerEvents: window.PointerEvent && ('ontouchstart' in window || window.navigator.maxTouchPoints > 0 || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0) }; diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index cbdf3775398..671c8d93d9e 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -16,6 +16,7 @@ import * as platform from 'vs/base/common/platform'; import { coalesce } from 'vs/base/common/arrays'; import { URI } from 'vs/base/common/uri'; import { Schemas, RemoteAuthorities } from 'vs/base/common/network'; +import { BrowserFeatures } from 'vs/base/browser/canIUse'; export function clearNode(node: HTMLElement): void { while (node.firstChild) { @@ -266,6 +267,13 @@ export let addStandardDisposableListener: IAddStandardDisposableListenerSignatur return addDisposableListener(node, type, wrapHandler, useCapture); }; +export function addDisposableGenericMouseDownListner(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable { + return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_DOWN : EventType.MOUSE_DOWN, handler, useCapture); +} + +export function addDisposableGenericMouseUpListner(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable { + return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_UP : EventType.MOUSE_UP, handler, useCapture); +} export function addDisposableNonBubblingMouseOutListener(node: Element, handler: (event: MouseEvent) => void): IDisposable { return addDisposableListener(node, 'mouseout', (e: MouseEvent) => { // Mouse out bubbles, so this is an attempt to ignore faux mouse outs coming from children elements @@ -443,7 +451,7 @@ export interface DOMEvent { } const MINIMUM_TIME_MS = 16; -const DEFAULT_EVENT_MERGER: IEventMerger = function (lastEvent: DOMEvent, currentEvent: DOMEvent) { +const DEFAULT_EVENT_MERGER: IEventMerger = function (lastEvent: DOMEvent | null, currentEvent: DOMEvent) { return currentEvent; }; @@ -492,6 +500,11 @@ export function getClientArea(element: HTMLElement): Dimension { return new Dimension(element.clientWidth, element.clientHeight); } + // If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight + if (platform.isIOS && (window).visualViewport) { + return new Dimension((window).visualViewport.width, (window).visualViewport.height); + } + // Try innerWidth / innerHeight if (window.innerWidth && window.innerHeight) { return new Dimension(window.innerWidth, window.innerHeight); diff --git a/src/vs/base/browser/globalMouseMoveMonitor.ts b/src/vs/base/browser/globalMouseMoveMonitor.ts index b24a3db0e50..c3c71d44fcb 100644 --- a/src/vs/base/browser/globalMouseMoveMonitor.ts +++ b/src/vs/base/browser/globalMouseMoveMonitor.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import * as platform from 'vs/base/common/platform'; import { IframeUtils } from 'vs/base/browser/iframe'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -16,7 +17,7 @@ export interface IStandardMouseMoveEventData { } export interface IEventMerger { - (lastEvent: R, currentEvent: MouseEvent): R; + (lastEvent: R | null, currentEvent: MouseEvent): R; } export interface IMouseMoveCallback { @@ -27,7 +28,7 @@ export interface IOnStopCallback { (): void; } -export function standardMouseMoveMerger(lastEvent: IStandardMouseMoveEventData, currentEvent: MouseEvent): IStandardMouseMoveEventData { +export function standardMouseMoveMerger(lastEvent: IStandardMouseMoveEventData | null, currentEvent: MouseEvent): IStandardMouseMoveEventData { let ev = new StandardMouseEvent(currentEvent); ev.preventDefault(); return { @@ -85,12 +86,12 @@ export class GlobalMouseMoveMonitor implements IDisposable { this.onStopCallback = onStopCallback; let windowChain = IframeUtils.getSameOriginWindowChain(); - const mouseMove = BrowserFeatures.pointerEvents ? 'pointermove' : 'mousemove'; - const mouseUp = BrowserFeatures.pointerEvents ? 'pointerup' : 'mouseup'; + const mouseMove = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointermove' : 'mousemove'; + const mouseUp = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointerup' : 'mouseup'; for (const element of windowChain) { this.hooks.add(dom.addDisposableThrottledListener(element.window.document, mouseMove, (data: R) => this.mouseMoveCallback!(data), - (lastEvent: R, currentEvent) => this.mouseMoveEventMerger!(lastEvent, currentEvent as MouseEvent) + (lastEvent: R | null, currentEvent) => this.mouseMoveEventMerger!(lastEvent, currentEvent as MouseEvent) )); this.hooks.add(dom.addDisposableListener(element.window.document, mouseUp, (e: MouseEvent) => this.stopMonitoring(true))); } diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 57144d67ea6..971addf8f00 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -50,19 +50,19 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const _href = function (href: string, isDomUri: boolean): string { const data = markdown.uris && markdown.uris[href]; if (!data) { - return href; + return href; // no uri exists } let uri = URI.revive(data); + if (URI.parse(href).toString() === uri.toString()) { + return href; // no tranformation performed + } if (isDomUri) { uri = DOM.asDomUri(uri); } if (uri.query) { uri = uri.with({ query: _uriMassage(uri.query) }); } - if (data) { - href = uri.toString(true); - } - return href; + return uri.toString(); }; // signal to code-block render that the diff --git a/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts index bef90a94d4d..912b17e1319 100644 --- a/src/vs/base/browser/touch.ts +++ b/src/vs/base/browser/touch.ts @@ -71,6 +71,7 @@ export class Gesture extends Disposable { private dispatched = false; private targets: HTMLElement[]; + private ignoreTargets: HTMLElement[]; private handle: IDisposable | null; private activeTouches: { [id: number]: TouchData; }; @@ -81,6 +82,7 @@ export class Gesture extends Disposable { this.activeTouches = {}; this.handle = null; this.targets = []; + this.ignoreTargets = []; this._register(DomUtils.addDisposableListener(document, 'touchstart', (e: TouchEvent) => this.onTouchStart(e))); this._register(DomUtils.addDisposableListener(document, 'touchend', (e: TouchEvent) => this.onTouchEnd(e))); this._register(DomUtils.addDisposableListener(document, 'touchmove', (e: TouchEvent) => this.onTouchMove(e))); @@ -103,6 +105,23 @@ export class Gesture extends Disposable { }; } + public static ignoreTarget(element: HTMLElement): IDisposable { + if (!Gesture.isTouchDevice()) { + return Disposable.None; + } + if (!Gesture.INSTANCE) { + Gesture.INSTANCE = new Gesture(); + } + + Gesture.INSTANCE.ignoreTargets.push(element); + + return { + dispose: () => { + Gesture.INSTANCE.ignoreTargets = Gesture.INSTANCE.ignoreTargets.filter(t => t !== element); + } + }; + } + @memoize private static isTouchDevice(): boolean { return 'ontouchstart' in window as any || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0; @@ -228,6 +247,12 @@ export class Gesture extends Disposable { } private dispatchEvent(event: GestureEvent): void { + for (let i = 0; i < this.ignoreTargets.length; i++) { + if (event.initialTarget instanceof Node && this.ignoreTargets[i].contains(event.initialTarget)) { + return; + } + } + this.targets.forEach(target => { if (event.initialTarget instanceof Node && target.contains(event.initialTarget)) { target.dispatchEvent(event); diff --git a/src/vs/base/browser/ui/checkbox/checkbox.ts b/src/vs/base/browser/ui/checkbox/checkbox.ts index afc9cc616a0..8eb398bf2db 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.ts +++ b/src/vs/base/browser/ui/checkbox/checkbox.ts @@ -113,6 +113,8 @@ export class Checkbox extends Widget { ev.preventDefault(); }); + this.ignoreGesture(this.domNode); + this.onkeydown(this.domNode, (keyboardEvent) => { if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) { this.checked = !this._checked; diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index beb97dd70d5..ef963fea473 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?72bd9e6bbf1e48287bcb9a9e4babeb28") format("truetype"); + src: url("./codicon.ttf?b035097bd976825411d2c57142be0530") format("truetype"); } .codicon[class*='codicon-'] { @@ -69,6 +69,9 @@ .codicon-eye-watch:before { content: "\ea70" } .codicon-circle-filled:before { content: "\ea71" } .codicon-primitive-dot:before { content: "\ea71" } +.codicon-debug-breakpoint:before { content: "\ea71" } +.codicon-debug-breakpoint-disabled:before { content: "\ea71" } +.codicon-debug-hint:before { content: "\ea71" } .codicon-primitive-square:before { content: "\ea72" } .codicon-edit:before { content: "\ea73" } .codicon-pencil:before { content: "\ea73" } @@ -162,12 +165,15 @@ .codicon-bold:before { content: "\eaa3" } .codicon-book:before { content: "\eaa4" } .codicon-bookmark:before { content: "\eaa5" } -.codicon-breakpoint-conditional-unverified:before { content: "\eaa6" } -.codicon-breakpoint-conditional:before { content: "\eaa7" } -.codicon-breakpoint-data-unverified:before { content: "\eaa8" } -.codicon-breakpoint-data:before { content: "\eaa9" } -.codicon-breakpoint-log-unverified:before { content: "\eaaa" } -.codicon-breakpoint-log:before { content: "\eaab" } +.codicon-debug-breakpoint-conditional-unverified:before { content: "\eaa6" } +.codicon-debug-breakpoint-conditional:before { content: "\eaa7" } +.codicon-debug-breakpoint-conditional-disabled:before { content: "\eaa7" } +.codicon-debug-breakpoint-data-unverified:before { content: "\eaa8" } +.codicon-debug-breakpoint-data:before { content: "\eaa9" } +.codicon-debug-breakpoint-data-disabled:before { content: "\eaa9" } +.codicon-debug-breakpoint-log-unverified:before { content: "\eaaa" } +.codicon-debug-breakpoint-log:before { content: "\eaab" } +.codicon-debug-breakpoint-log-disabled:before { content: "\eaab" } .codicon-briefcase:before { content: "\eaac" } .codicon-broadcast:before { content: "\eaad" } .codicon-browser:before { content: "\eaae" } @@ -185,6 +191,7 @@ .codicon-chrome-minimize:before { content: "\eaba" } .codicon-chrome-restore:before { content: "\eabb" } .codicon-circle-outline:before { content: "\eabc" } +.codicon-debug-breakpoint-unverified:before { content: "\eabc" } .codicon-circle-slash:before { content: "\eabd" } .codicon-circuit-board:before { content: "\eabe" } .codicon-clear-all:before { content: "\eabf" } @@ -198,8 +205,6 @@ .codicon-comment-discussion:before { content: "\eac7" } .codicon-compare-changes:before { content: "\eac8" } .codicon-credit-card:before { content: "\eac9" } -.codicon-current-and-breakpoint:before { content: "\eaca" } -.codicon-current:before { content: "\eacb" } .codicon-dash:before { content: "\eacc" } .codicon-dashboard:before { content: "\eacd" } .codicon-database:before { content: "\eace" } @@ -387,3 +392,12 @@ .codicon-list-selection:before { content: "\eb85" } .codicon-selection:before { content: "\eb85" } .codicon-list-tree:before { content: "\eb86" } +.codicon-debug-breakpoint-function-unverified:before { content: "\eb87" } +.codicon-debug-breakpoint-function:before { content: "\eb88" } +.codicon-debug-breakpoint-function-disabled:before { content: "\eb88" } +.codicon-debug-breakpoint-stackframe-active:before { content: "\eb89" } +.codicon-debug-breakpoint-stackframe-dot:before { content: "\eb8a" } +.codicon-debug-breakpoint-stackframe:before { content: "\eb8b" } +.codicon-debug-breakpoint-stackframe-focused:before { content: "\eb8b" } +.codicon-debug-breakpoint-unsupported:before { content: "\eb8c" } +.codicon-debug-step-back:before { content: "\f101" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index d39e8ad92fd..e2d73d87e70 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 54c9954259b..eacf37f358a 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -5,6 +5,7 @@ import 'vs/css!./contextview'; import * as DOM from 'vs/base/browser/dom'; +import * as platform from 'vs/base/common/platform'; import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Range } from 'vs/base/common/range'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; @@ -179,7 +180,7 @@ export class ContextView extends Disposable { return; } - if (this.delegate!.canRelayout === false && !BrowserFeatures.pointerEvents) { + if (this.delegate!.canRelayout === false && !(platform.isIOS && BrowserFeatures.pointerEvents)) { this.hide(); return; } diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 3fded36de56..40e7ae8790f 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -8,6 +8,7 @@ import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IMatch } from 'vs/base/common/filters'; import { Disposable } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/base/common/range'; export interface IIconLabelCreationOptions { supportHighlights?: boolean; @@ -24,6 +25,7 @@ export interface IIconLabelValueOptions { matches?: IMatch[]; labelEscapeNewLines?: boolean; descriptionMatches?: IMatch[]; + readonly separator?: string; } class FastLabelNode { @@ -86,9 +88,10 @@ class FastLabelNode { } export class IconLabel extends Disposable { + private domNode: FastLabelNode; - private labelDescriptionContainer: FastLabelNode; - private labelNode: FastLabelNode | HighlightedLabel; + private descriptionContainer: FastLabelNode; + private nameNode: Label | LabelWithHighlights; private descriptionNode: FastLabelNode | HighlightedLabel | undefined; private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; @@ -97,18 +100,21 @@ export class IconLabel extends Disposable { this.domNode = this._register(new FastLabelNode(dom.append(container, dom.$('.monaco-icon-label')))); - this.labelDescriptionContainer = this._register(new FastLabelNode(dom.append(this.domNode.element, dom.$('.monaco-icon-label-description-container')))); + const labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container')); + + const nameContainer = dom.append(labelContainer, dom.$('span.monaco-icon-name-container')); + this.descriptionContainer = this._register(new FastLabelNode(dom.append(labelContainer, dom.$('span.monaco-icon-description-container')))); if (options?.supportHighlights) { - this.labelNode = new HighlightedLabel(dom.append(this.labelDescriptionContainer.element, dom.$('a.label-name')), !!options.supportCodicons); + this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons); } else { - this.labelNode = this._register(new FastLabelNode(dom.append(this.labelDescriptionContainer.element, dom.$('a.label-name')))); + this.nameNode = new Label(nameContainer); } if (options?.supportDescriptionHighlights) { - this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.labelDescriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons); + this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons); } else { - this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.labelDescriptionContainer.element, dom.$('span.label-description')))); + this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description')))); } } @@ -116,7 +122,7 @@ export class IconLabel extends Disposable { return this.domNode.element; } - setLabel(label: string, description?: string, options?: IIconLabelValueOptions): void { + setLabel(label: string | string[], description?: string, options?: IIconLabelValueOptions): void { const classes = ['monaco-icon-label']; if (options) { if (options.extraClasses) { @@ -131,11 +137,7 @@ export class IconLabel extends Disposable { this.domNode.className = classes.join(' '); this.domNode.title = options?.title || ''; - if (this.labelNode instanceof HighlightedLabel) { - this.labelNode.set(label || '', options?.matches, options?.title, options?.labelEscapeNewLines); - } else { - this.labelNode.textContent = label || ''; - } + this.nameNode.setLabel(label, options); if (description || this.descriptionNode) { if (!this.descriptionNode) { @@ -157,3 +159,110 @@ export class IconLabel extends Disposable { } } } + +class Label { + + private label: string | string[] | undefined = undefined; + private singleLabel: HTMLElement | undefined = undefined; + + constructor(private container: HTMLElement) { } + + setLabel(label: string | string[], options?: IIconLabelValueOptions): void { + if (this.label === label) { + return; + } + + this.label = label; + + if (typeof label === 'string') { + if (!this.singleLabel) { + this.container.innerHTML = ''; + dom.removeClass(this.container, 'multiple'); + this.singleLabel = dom.append(this.container, dom.$('a.label-name')); + } + + this.singleLabel.textContent = label; + } else { + this.container.innerHTML = ''; + dom.addClass(this.container, 'multiple'); + this.singleLabel = undefined; + + for (let i = 0; i < label.length; i++) { + const l = label[i]; + + dom.append(this.container, dom.$('a.label-name', { 'data-icon-label-count': label.length, 'data-icon-label-index': i }, l)); + + if (i < label.length - 1) { + dom.append(this.container, dom.$('span.label-separator', undefined, options?.separator || '/')); + } + } + } + } +} + +function splitMatches(labels: string[], separator: string, matches: IMatch[] | undefined): IMatch[][] | undefined { + if (!matches) { + return undefined; + } + + let labelStart = 0; + + return labels.map(label => { + const labelRange = { start: labelStart, end: labelStart + label.length }; + + const result = matches + .map(match => Range.intersect(labelRange, match)) + .filter(range => !Range.isEmpty(range)) + .map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart })); + + labelStart = labelRange.end + separator.length; + return result; + }); +} + +class LabelWithHighlights { + + private label: string | string[] | undefined = undefined; + private singleLabel: HighlightedLabel | undefined = undefined; + + constructor(private container: HTMLElement, private supportCodicons: boolean) { } + + setLabel(label: string | string[], options?: IIconLabelValueOptions): void { + if (this.label === label) { + return; + } + + this.label = label; + + if (typeof label === 'string') { + if (!this.singleLabel) { + this.container.innerHTML = ''; + dom.removeClass(this.container, 'multiple'); + this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name')), this.supportCodicons); + } + + this.singleLabel.set(label, options?.matches, options?.title, options?.labelEscapeNewLines); + } else { + + this.container.innerHTML = ''; + dom.addClass(this.container, 'multiple'); + this.singleLabel = undefined; + + const separator = options?.separator || '/'; + const matches = splitMatches(label, separator, options?.matches); + + for (let i = 0; i < label.length; i++) { + const l = label[i]; + const m = matches ? matches[i] : undefined; + + const name = dom.$('a.label-name', { 'data-icon-label-count': label.length, 'data-icon-label-index': i }); + const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons); + highlightedLabel.set(l, m, options?.title, options?.labelEscapeNewLines); + + if (i < label.length - 1) { + dom.append(name, dom.$('span.label-separator', undefined, separator)); + } + } + } + } +} diff --git a/src/vs/base/browser/ui/iconLabel/iconlabel.css b/src/vs/base/browser/ui/iconLabel/iconlabel.css index ad21f0ae1e4..8ee16195b5c 100644 --- a/src/vs/base/browser/ui/iconLabel/iconlabel.css +++ b/src/vs/base/browser/ui/iconLabel/iconlabel.css @@ -31,25 +31,32 @@ flex-shrink: 0; /* fix for https://github.com/Microsoft/vscode/issues/13787 */ } -.monaco-icon-label > .monaco-icon-label-description-container { - overflow: hidden; /* this causes the label/description to shrink first if decorations are enabled */ +.monaco-icon-label > .monaco-icon-label-container { + min-width: 0; + overflow: hidden; text-overflow: ellipsis; + flex: 1; } -.monaco-icon-label > .monaco-icon-label-description-container > .label-name { +.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name { color: inherit; white-space: pre; /* enable to show labels that include multiple whitespaces */ } -.monaco-icon-label > .monaco-icon-label-description-container > .label-description { +.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name > .label-separator { + margin: 0 2px; + opacity: 0.5; +} + +.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description { opacity: .7; margin-left: 0.5em; font-size: 0.9em; white-space: pre; /* enable to show labels that include multiple whitespaces */ } -.monaco-icon-label.italic > .monaco-icon-label-description-container > .label-name, -.monaco-icon-label.italic > .monaco-icon-label-description-container > .label-description { +.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-name-container > .label-name, +.monaco-icon-label.italic > .monaco-icon-description-container > .label-description { font-style: italic; } @@ -58,7 +65,6 @@ font-size: 90%; font-weight: 600; padding: 0 16px 0 5px; - margin-left: auto; text-align: center; } diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 59dd50015d8..28d20f9f371 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -220,6 +220,8 @@ export class InputBox extends Widget { }); } + this.ignoreGesture(this.input); + setTimeout(() => this.updateMirror(), 0); // Support actions diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 076d78d7725..0844ddf65a4 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -103,10 +103,11 @@ export const ListDragOverReactions = { export interface IListDragAndDrop { getDragURI(element: T): string | null; - getDragLabel?(elements: T[]): string | undefined; + getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined; onDragStart?(data: IDragAndDropData, originalEvent: DragEvent): void; onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction; drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void; + onDragEnd?(originalEvent: DragEvent): void; } export class ListError extends Error { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 770bd8c4c34..8280f4e05c4 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -74,9 +74,10 @@ const DefaultOptions = { horizontalScrolling: false }; -export class ElementsDragAndDropData implements IDragAndDropData { +export class ElementsDragAndDropData implements IDragAndDropData { readonly elements: T[]; + context: TContext | undefined; constructor(elements: T[]) { this.elements = elements; @@ -84,7 +85,7 @@ export class ElementsDragAndDropData implements IDragAndDropData { update(): void { } - getData(): any { + getData(): T[] { return this.elements; } } @@ -99,7 +100,7 @@ export class ExternalElementsDragAndDropData implements IDragAndDropData { update(): void { } - getData(): any { + getData(): T[] { return this.elements; } } @@ -766,7 +767,7 @@ export class ListView implements ISpliceable, IDisposable { let label: string | undefined; if (this.dnd.getDragLabel) { - label = this.dnd.getDragLabel(elements); + label = this.dnd.getDragLabel(elements, event); } if (typeof label === 'undefined') { @@ -846,10 +847,6 @@ export class ListView implements ISpliceable, IDisposable { feedback = distinct(feedback).filter(i => i >= -1 && i < this.length).sort(); feedback = feedback[0] === -1 ? [-1] : feedback; - if (feedback.length === 0) { - throw new Error('Invalid empty feedback list'); - } - if (equalsDragFeedback(this.currentDragFeedback, feedback)) { return true; } @@ -910,12 +907,16 @@ export class ListView implements ISpliceable, IDisposable { this.dnd.drop(dragData, event.element, event.index, event.browserEvent); } - private onDragEnd(): void { + private onDragEnd(event: DragEvent): void { this.canDrop = false; this.teardownDragAndDropScrollTopAnimation(); this.clearDragOverFeedback(); this.currentDragData = undefined; StaticDND.CurrentDragAndDropData = undefined; + + if (this.dnd.onDragEnd) { + this.dnd.onDragEnd(event); + } } private clearDragOverFeedback(): void { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 65ff0de7f6e..f928b7dc2f7 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1062,9 +1062,9 @@ class ListViewDragAndDrop implements IListViewDragAndDrop { return this.dnd.getDragURI(element); } - getDragLabel?(elements: T[]): string | undefined { + getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined { if (this.dnd.getDragLabel) { - return this.dnd.getDragLabel(elements); + return this.dnd.getDragLabel(elements, originalEvent); } return undefined; @@ -1080,6 +1080,12 @@ class ListViewDragAndDrop implements IListViewDragAndDrop { return this.dnd.onDragOver(data, targetElement, targetIndex, originalEvent); } + onDragEnd(originalEvent: DragEvent): void { + if (this.dnd.onDragEnd) { + this.dnd.onDragEnd(originalEvent); + } + } + drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void { this.dnd.drop(data, targetElement, targetIndex, originalEvent); } diff --git a/src/vs/base/browser/ui/selectBox/selectBoxNative.ts b/src/vs/base/browser/ui/selectBox/selectBoxNative.ts index 8c50b057fb5..fc18c6d3b18 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxNative.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxNative.ts @@ -10,6 +10,7 @@ import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; import { ISelectBoxDelegate, ISelectOptionItem, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox'; import { isMacintosh } from 'vs/base/common/platform'; +import { Gesture, EventType } from 'vs/base/browser/touch'; export class SelectBoxNative extends Disposable implements ISelectBoxDelegate { @@ -43,6 +44,12 @@ export class SelectBoxNative extends Disposable implements ISelectBoxDelegate { } private registerListeners() { + this._register(Gesture.addTarget(this.selectElement)); + [EventType.Tap].forEach(eventType => { + this._register(dom.addDisposableListener(this.selectElement, eventType, (e) => { + this.selectElement.focus(); + })); + }); this._register(dom.addStandardDisposableListener(this.selectElement, 'change', (e) => { this.selectElement.title = e.target.value; diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index f47c7cf804f..addcf81a6ee 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -27,10 +27,24 @@ import { clamp } from 'vs/base/common/numbers'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { SetMap } from 'vs/base/common/collections'; +class TreeElementsDragAndDropData extends ElementsDragAndDropData { + + set context(context: TContext | undefined) { + this.data.context = context; + } + + get context(): TContext | undefined { + return this.data.context; + } + + constructor(private data: ElementsDragAndDropData, TContext>) { + super(data.elements.map(node => node.element)); + } +} + function asTreeDragAndDropData(data: IDragAndDropData): IDragAndDropData { if (data instanceof ElementsDragAndDropData) { - const nodes = (data as ElementsDragAndDropData>).elements; - return new ElementsDragAndDropData(nodes.map(node => node.element)); + return new TreeElementsDragAndDropData(data); } return data; @@ -47,9 +61,9 @@ class TreeNodeListDragAndDrop implements IListDragAndDrop< return this.dnd.getDragURI(node.element); } - getDragLabel(nodes: ITreeNode[]): string | undefined { + getDragLabel(nodes: ITreeNode[], originalEvent: DragEvent): string | undefined { if (this.dnd.getDragLabel) { - return this.dnd.getDragLabel(nodes.map(node => node.element)); + return this.dnd.getDragLabel(nodes.map(node => node.element), originalEvent); } return undefined; @@ -87,7 +101,7 @@ class TreeNodeListDragAndDrop implements IListDragAndDrop< }, 500); } - if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined') { + if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined' || result.feedback) { if (!raw) { const accept = typeof result === 'boolean' ? result : result.accept; const effect = typeof result === 'boolean' ? undefined : result.effect; @@ -121,6 +135,12 @@ class TreeNodeListDragAndDrop implements IListDragAndDrop< this.dnd.drop(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, originalEvent); } + + onDragEnd(originalEvent: DragEvent): void { + if (this.dnd.onDragEnd) { + this.dnd.onDragEnd(originalEvent); + } + } } function asListOptions(modelProvider: () => ITreeModel, options?: IAbstractTreeOptions): IListOptions> | undefined { diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 19c5c94724f..847b185af31 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -150,10 +150,24 @@ function asTreeContextMenuEvent(e: ITreeContextMenuEvent extends ElementsDragAndDropData { + + set context(context: TContext | undefined) { + this.data.context = context; + } + + get context(): TContext | undefined { + return this.data.context; + } + + constructor(private data: ElementsDragAndDropData, TContext>) { + super(data.elements.map(node => node.element as T)); + } +} + function asAsyncDataTreeDragAndDropData(data: IDragAndDropData): IDragAndDropData { if (data instanceof ElementsDragAndDropData) { - const nodes = (data as ElementsDragAndDropData>).elements; - return new ElementsDragAndDropData(nodes.map(node => node.element)); + return new AsyncDataTreeElementsDragAndDropData(data); } return data; @@ -167,9 +181,9 @@ class AsyncDataTreeNodeListDragAndDrop implements IListDragAndDrop[]): string | undefined { + getDragLabel(nodes: IAsyncDataTreeNode[], originalEvent: DragEvent): string | undefined { if (this.dnd.getDragLabel) { - return this.dnd.getDragLabel(nodes.map(node => node.element as T)); + return this.dnd.getDragLabel(nodes.map(node => node.element as T), originalEvent); } return undefined; @@ -188,6 +202,12 @@ class AsyncDataTreeNodeListDragAndDrop implements IListDragAndDrop | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void { this.dnd.drop(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element as T, targetIndex, originalEvent); } + + onDragEnd(originalEvent: DragEvent): void { + if (this.dnd.onDragEnd) { + this.dnd.onDragEnd(originalEvent); + } + } } function asObjectTreeOptions(options?: IAsyncDataTreeOptions): IObjectTreeOptions, TFilterData> | undefined { @@ -993,6 +1013,12 @@ class CompressibleAsyncDataTreeRenderer i } } + disposeCompressedElements(node: ITreeNode>, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { + if (this.renderer.disposeCompressedElements) { + this.renderer.disposeCompressedElements(this.compressibleNodeMapperProvider().map(node) as ITreeNode, TFilterData>, index, templateData.templateData, height); + } + } + disposeTemplate(templateData: IDataTreeListTemplateData): void { this.renderer.disposeTemplate(templateData.templateData); } diff --git a/src/vs/base/browser/ui/widget.ts b/src/vs/base/browser/ui/widget.ts index 569ef4925fc..7e44d9a5a4b 100644 --- a/src/vs/base/browser/ui/widget.ts +++ b/src/vs/base/browser/ui/widget.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Disposable } from 'vs/base/common/lifecycle'; +import { Gesture } from 'vs/base/browser/touch'; export abstract class Widget extends Disposable { @@ -49,4 +50,8 @@ export abstract class Widget extends Disposable { protected onchange(domNode: HTMLElement, listener: (e: Event) => void): void { this._register(dom.addDisposableListener(domNode, dom.EventType.CHANGE, listener)); } + + protected ignoreGesture(domNode: HTMLElement): void { + Gesture.ignoreTarget(domNode); + } } diff --git a/src/vs/base/common/path.ts b/src/vs/base/common/path.ts index 28b7d5e61cc..5f1739053bb 100644 --- a/src/vs/base/common/path.ts +++ b/src/vs/base/common/path.ts @@ -69,11 +69,11 @@ function validateString(value: string, name: string) { } } -function isPathSeparator(code: number) { +function isPathSeparator(code: number | undefined) { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; } -function isPosixPathSeparator(code: number) { +function isPosixPathSeparator(code: number | undefined) { return code === CHAR_FORWARD_SLASH; } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index b3c7001a51d..5a631e0b395 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -10,6 +10,7 @@ let _isMacintosh = false; let _isLinux = false; let _isNative = false; let _isWeb = false; +let _isIOS = false; let _locale: string | undefined = undefined; let _language: string = LANGUAGE_DEFAULT; let _translationsConfigFile: string | undefined = undefined; @@ -41,6 +42,7 @@ declare const global: any; interface INavigator { userAgent: string; language: string; + maxTouchPoints?: number; } declare const navigator: INavigator; declare const self: any; @@ -52,6 +54,7 @@ if (typeof navigator === 'object' && !isElectronRenderer) { _userAgent = navigator.userAgent; _isWindows = _userAgent.indexOf('Windows') >= 0; _isMacintosh = _userAgent.indexOf('Macintosh') >= 0; + _isIOS = _userAgent.indexOf('Macintosh') >= 0 && !!navigator.maxTouchPoints && navigator.maxTouchPoints > 0; _isLinux = _userAgent.indexOf('Linux') >= 0; _isWeb = true; _locale = navigator.language; @@ -106,6 +109,7 @@ export const isMacintosh = _isMacintosh; export const isLinux = _isLinux; export const isNative = _isNative; export const isWeb = _isWeb; +export const isIOS = _isIOS; export const platform = _platform; export const userAgent = _userAgent; diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 7ae960dbc91..af6317d035f 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -32,6 +32,7 @@ 'xterm': `${window.location.origin}/static/remote/web/node_modules/xterm/lib/xterm.js`, 'xterm-addon-search': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, 'xterm-addon-web-links': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, + 'xterm-addon-webgl': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, 'semver-umd': `${window.location.origin}/static/remote/web/node_modules/semver-umd/lib/semver-umd.js`, } }; diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index dbcb8e490fe..8a6a9f54e67 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -36,6 +36,7 @@ 'xterm': `${window.location.origin}/static/node_modules/xterm/lib/xterm.js`, 'xterm-addon-search': `${window.location.origin}/static/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, 'xterm-addon-web-links': `${window.location.origin}/static/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, + 'xterm-addon-webgl': `${window.location.origin}/static/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, 'semver-umd': `${window.location.origin}/static/node_modules/semver-umd/lib/semver-umd.js`, } }; diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index b55f4095e3b..83e72ebd51b 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -438,15 +438,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; - this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => { - this.marketplaceHeadersPromise.then(headers => { - const requestHeaders = objects.assign(details.requestHeaders, headers) as { [key: string]: string | undefined }; - if (!this.configurationService.getValue('extensions.disableExperimentalAzureSearch')) { - requestHeaders['Cookie'] = `${requestHeaders['Cookie'] ? requestHeaders['Cookie'] + ';' : ''}EnableExternalSearchForVSCode=true`; - } - cb({ cancel: false, requestHeaders }); - }); - }); + this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => + this.marketplaceHeadersPromise.then(headers => cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) as { [key: string]: string | undefined } }))); } private onWindowError(error: WindowError): void { @@ -1096,7 +1089,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private createTouchBarGroupSegments(items: ISerializableCommandAction[] = []): ITouchBarSegment[] { const segments: ITouchBarSegment[] = items.map(item => { let icon: NativeImage | undefined; - if (item.iconLocation && item.iconLocation.dark.scheme === 'file') { + if (item.iconLocation && item.iconLocation?.dark?.scheme === 'file') { icon = nativeImage.createFromPath(URI.revive(item.iconLocation.dark).fsPath); if (icon.isEmpty()) { icon = undefined; diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 8c8bb743462..d235e6ba791 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -27,7 +27,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; * Merges mouse events when mouse move events are throttled */ export function createMouseMoveEventMerger(mouseTargetFactory: MouseTargetFactory | null) { - return function (lastEvent: EditorMouseEvent, currentEvent: EditorMouseEvent): EditorMouseEvent { + return function (lastEvent: EditorMouseEvent | null, currentEvent: EditorMouseEvent): EditorMouseEvent { let targetIsWidget = false; if (mouseTargetFactory) { targetIsWidget = mouseTargetFactory.mouseTargetIsWidget(currentEvent); diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index 28ebcf990af..30ab2b51656 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import * as platform from 'vs/base/common/platform'; import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IPointerHandlerHelper, MouseHandler, createMouseMoveEventMerger } from 'vs/editor/browser/controller/mouseHandler'; @@ -18,7 +19,7 @@ interface IThrottledGestureEvent { translationY: number; } -function gestureChangeEventMerger(lastEvent: IThrottledGestureEvent, currentEvent: MSGestureEvent): IThrottledGestureEvent { +function gestureChangeEventMerger(lastEvent: IThrottledGestureEvent | null, currentEvent: MSGestureEvent): IThrottledGestureEvent { const r = { translationY: currentEvent.translationY, translationX: currentEvent.translationX @@ -53,7 +54,7 @@ class MsPointerHandler extends MouseHandler implements IDisposable { const penGesture = new MSGesture(); touchGesture.target = this.viewHelper.linesContentDomNode; penGesture.target = this.viewHelper.linesContentDomNode; - this.viewHelper.linesContentDomNode.addEventListener('MSPointerDown', (e: MSPointerEvent) => { + this.viewHelper.linesContentDomNode.addEventListener('MSPointerDown', (e: MSPointerEvent) => { // Circumvent IE11 breaking change in e.pointerType & TypeScript's stale definitions const pointerType = e.pointerType; if (pointerType === ((e).MSPOINTER_TYPE_MOUSE || 'mouse')) { @@ -67,7 +68,7 @@ class MsPointerHandler extends MouseHandler implements IDisposable { penGesture.addPointer(e.pointerId); } }); - this._register(dom.addDisposableThrottledListener(this.viewHelper.linesContentDomNode, 'MSGestureChange', (e) => this._onGestureChange(e), gestureChangeEventMerger)); + this._register(dom.addDisposableThrottledListener(this.viewHelper.linesContentDomNode, 'MSGestureChange', (e) => this._onGestureChange(e), gestureChangeEventMerger)); this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, 'MSGestureTap', (e) => this._onCaptureGestureTap(e), true)); } }, 100); @@ -132,7 +133,7 @@ class StandardPointerHandler extends MouseHandler implements IDisposable { const penGesture = new MSGesture(); touchGesture.target = this.viewHelper.linesContentDomNode; penGesture.target = this.viewHelper.linesContentDomNode; - this.viewHelper.linesContentDomNode.addEventListener('pointerdown', (e: MSPointerEvent) => { + this.viewHelper.linesContentDomNode.addEventListener('pointerdown', (e: PointerEvent) => { const pointerType = e.pointerType; if (pointerType === 'mouse') { this._lastPointerType = 'mouse'; @@ -145,7 +146,7 @@ class StandardPointerHandler extends MouseHandler implements IDisposable { penGesture.addPointer(e.pointerId); } }); - this._register(dom.addDisposableThrottledListener(this.viewHelper.linesContentDomNode, 'MSGestureChange', (e) => this._onGestureChange(e), gestureChangeEventMerger)); + this._register(dom.addDisposableThrottledListener(this.viewHelper.linesContentDomNode, 'MSGestureChange', (e) => this._onGestureChange(e), gestureChangeEventMerger)); this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, 'MSGestureTap', (e) => this._onCaptureGestureTap(e), true)); } }, 100); @@ -283,7 +284,7 @@ export class PointerHandler extends Disposable { super(); if (window.navigator.msPointerEnabled) { this.handler = this._register(new MsPointerHandler(context, viewController, viewHelper)); - } else if (((window).PointerEvent && BrowserFeatures.pointerEvents)) { + } else if ((platform.isIOS && BrowserFeatures.pointerEvents)) { this.handler = this._register(new PointerEventHandler(context, viewController, viewHelper)); } else if ((window).TouchEvent) { this.handler = this._register(new TouchHandler(context, viewController, viewHelper)); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 50eed2efb08..bcd0995f843 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -16,7 +16,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer } from 'vs/editor/common/model'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; -import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; +import { IEditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IDiffComputationResult } from 'vs/editor/common/services/editorWorkerService'; diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 60df7e8f1b8..9401fdd259e 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -84,7 +84,7 @@ export class EditorMouseEvent extends StandardMouseEvent { } export interface EditorMouseEventMerger { - (lastEvent: EditorMouseEvent, currentEvent: EditorMouseEvent): EditorMouseEvent; + (lastEvent: EditorMouseEvent | null, currentEvent: EditorMouseEvent): EditorMouseEvent; } export class EditorMouseEventFactory { @@ -124,7 +124,7 @@ export class EditorMouseEventFactory { } public onMouseMoveThrottled(target: HTMLElement, callback: (e: EditorMouseEvent) => void, merger: EditorMouseEventMerger, minimumTimeMs: number): IDisposable { - const myMerger: dom.IEventMerger = (lastEvent: EditorMouseEvent, currentEvent: MouseEvent): EditorMouseEvent => { + const myMerger: dom.IEventMerger = (lastEvent: EditorMouseEvent | null, currentEvent: MouseEvent): EditorMouseEvent => { return merger(lastEvent, this._create(currentEvent)); }; return dom.addDisposableThrottledListener(target, 'mousemove', callback, myMerger, minimumTimeMs); @@ -162,7 +162,7 @@ export class EditorPointerEventFactory { } public onPointerMoveThrottled(target: HTMLElement, callback: (e: EditorMouseEvent) => void, merger: EditorMouseEventMerger, minimumTimeMs: number): IDisposable { - const myMerger: dom.IEventMerger = (lastEvent: EditorMouseEvent, currentEvent: MouseEvent): EditorMouseEvent => { + const myMerger: dom.IEventMerger = (lastEvent: EditorMouseEvent | null, currentEvent: MouseEvent): EditorMouseEvent => { return merger(lastEvent, this._create(currentEvent)); }; return dom.addDisposableThrottledListener(target, 'pointermove', callback, myMerger, minimumTimeMs); @@ -195,7 +195,7 @@ export class GlobalEditorMouseMoveMonitor extends Disposable { this._globalMouseMoveMonitor.stopMonitoring(true); }, true); - const myMerger: dom.IEventMerger = (lastEvent: EditorMouseEvent, currentEvent: MouseEvent): EditorMouseEvent => { + const myMerger: dom.IEventMerger = (lastEvent: EditorMouseEvent | null, currentEvent: MouseEvent): EditorMouseEvent => { return merger(lastEvent, new EditorMouseEvent(currentEvent, this._editorViewDomNode)); }; diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index 1b81608b013..2a12d008b5c 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -307,7 +307,7 @@ export function registerEditorContribution(id EditorContributionRegistry.INSTANCE.registerEditorContribution(id, ctor); } -export function registerDiffEditorContribution(id: string, ctor: IDiffEditorContributionCtor): void { +export function registerDiffEditorContribution(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void { EditorContributionRegistry.INSTANCE.registerDiffEditorContribution(id, ctor); } @@ -363,7 +363,7 @@ class EditorContributionRegistry { return this.editorContributions.slice(0); } - public registerDiffEditorContribution(id: string, ctor: IDiffEditorContributionCtor): void { + public registerDiffEditorContribution(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void { this.diffEditorContributions.push({ id, ctor }); } diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 48175adfb42..ead9bd31bfb 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -4,58 +4,132 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; -import * as resources from 'vs/base/common/resources'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { normalizePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener } from 'vs/platform/opener/common/opener'; +import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener, matchesScheme } from 'vs/platform/opener/common/opener'; import { EditorOpenContext } from 'vs/platform/editor/common/editor'; -export class OpenerService extends Disposable implements IOpenerService { + +class CommandOpener implements IOpener { + + constructor(@ICommandService private readonly _commandService: ICommandService) { } + + async open(target: URI | string) { + if (!matchesScheme(target, Schemas.command)) { + return false; + } + // run command or bail out if command isn't known + if (typeof target === 'string') { + target = URI.parse(target); + } + if (!CommandsRegistry.getCommand(target.path)) { + throw new Error(`command '${target.path}' NOT known`); + } + // execute as command + let args: any = []; + try { + args = parse(target.query); + if (!Array.isArray(args)) { + args = [args]; + } + } catch (e) { + // ignore error + } + await this._commandService.executeCommand(target.path, ...args); + return true; + } +} + +class EditorOpener implements IOpener { + + constructor(@ICodeEditorService private readonly _editorService: ICodeEditorService) { } + + async open(target: URI | string, options: OpenOptions) { + if (typeof target === 'string') { + target = URI.parse(target); + } + let selection: { startLineNumber: number; startColumn: number; } | undefined = undefined; + const match = /^L?(\d+)(?:,(\d+))?/.exec(target.fragment); + if (match) { + // support file:///some/file.js#73,84 + // support file:///some/file.js#L73 + selection = { + startLineNumber: parseInt(match[1]), + startColumn: match[2] ? parseInt(match[2]) : 1 + }; + // remove fragment + target = target.with({ fragment: '' }); + } + + if (target.scheme === Schemas.file) { + target = normalizePath(target); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954) + } + + await this._editorService.openCodeEditor( + { resource: target, options: { selection, context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API } }, + this._editorService.getFocusedCodeEditor(), + options?.openToSide + ); + + return true; + } +} + +export class OpenerService implements IOpenerService { _serviceBrand: undefined; private readonly _openers = new LinkedList(); private readonly _validators = new LinkedList(); private readonly _resolvers = new LinkedList(); + private _externalOpener: IExternalOpener; constructor( - @ICodeEditorService private readonly _editorService: ICodeEditorService, - @ICommandService private readonly _commandService: ICommandService, + @ICodeEditorService editorService: ICodeEditorService, + @ICommandService commandService: ICommandService, ) { - super(); - // Default external opener is going through window.open() this._externalOpener = { openExternal: href => { dom.windowOpenNoOpener(href); - return Promise.resolve(true); } }; + + // Default opener: maito, http(s), command, and catch-all-editors + this._openers.push({ + open: async (target: URI | string, options?: OpenOptions) => { + if (options?.openExternal || matchesScheme(target, Schemas.mailto) || matchesScheme(target, Schemas.http) || matchesScheme(target, Schemas.https)) { + // open externally + await this._doOpenExternal(target, options); + return true; + } + return false; + } + }); + this._openers.push(new CommandOpener(commandService)); + this._openers.push(new EditorOpener(editorService)); } registerOpener(opener: IOpener): IDisposable { - const remove = this._openers.push(opener); - + const remove = this._openers.unshift(opener); return { dispose: remove }; } registerValidator(validator: IValidator): IDisposable { const remove = this._validators.push(validator); - return { dispose: remove }; } registerExternalUriResolver(resolver: IExternalUriResolver): IDisposable { const remove = this._resolvers.push(resolver); - return { dispose: remove }; } @@ -63,30 +137,24 @@ export class OpenerService extends Disposable implements IOpenerService { this._externalOpener = externalOpener; } - async open(resource: URI, options?: OpenOptions): Promise { - - // no scheme ?!? - if (!resource.scheme) { - return Promise.resolve(false); - } + async open(target: URI | string, options?: OpenOptions): Promise { // check with contributed validators for (const validator of this._validators.toArray()) { - if (!(await validator.shouldOpen(resource))) { + if (!(await validator.shouldOpen(target))) { return false; } } // check with contributed openers for (const opener of this._openers.toArray()) { - const handled = await opener.open(resource, options); + const handled = await opener.open(target, options); if (handled) { return true; } } - // use default openers - return this._doOpen(resource, options); + return false; } async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise { @@ -100,68 +168,19 @@ export class OpenerService extends Disposable implements IOpenerService { return { resolved: resource, dispose: () => { } }; } - private async _doOpen(resource: URI, options: OpenOptions | undefined): Promise { - const { scheme, path, query, fragment } = resource; + private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise { - if (options?.openExternal || equalsIgnoreCase(scheme, Schemas.mailto) || equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https)) { - // open externally - return this._doOpenExternal(resource, options); + //todo@joh IExternalUriResolver should support `uri: URI | string` + const uri = typeof resource === 'string' ? URI.parse(resource) : resource; + const { resolved } = await this.resolveExternalUri(uri, options); + + if (typeof resource === 'string' && uri.toString() === resolved.toString()) { + // open the url-string AS IS + return this._externalOpener.openExternal(resource); + } else { + // open URI using the toString(noEncode)+encodeURI-trick + return this._externalOpener.openExternal(encodeURI(resolved.toString(true))); } - - if (equalsIgnoreCase(scheme, Schemas.command)) { - // run command or bail out if command isn't known - if (!CommandsRegistry.getCommand(path)) { - throw new Error(`command '${path}' NOT known`); - } - // execute as command - let args: any = []; - try { - args = parse(query); - if (!Array.isArray(args)) { - args = [args]; - } - } catch (e) { - // ignore error - } - - await this._commandService.executeCommand(path, ...args); - - return true; - } - - // finally open in editor - let selection: { startLineNumber: number; startColumn: number; } | undefined = undefined; - const match = /^L?(\d+)(?:,(\d+))?/.exec(fragment); - if (match) { - // support file:///some/file.js#73,84 - // support file:///some/file.js#L73 - selection = { - startLineNumber: parseInt(match[1]), - startColumn: match[2] ? parseInt(match[2]) : 1 - }; - // remove fragment - resource = resource.with({ fragment: '' }); - } - - if (resource.scheme === Schemas.file) { - resource = resources.normalizePath(resource); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954) - } - - await this._editorService.openCodeEditor( - { resource, options: { selection, context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API } }, - this._editorService.getFocusedCodeEditor(), - options?.openToSide - ); - - return true; - } - - private async _doOpenExternal(resource: URI, options: OpenOptions | undefined): Promise { - const { resolved } = await this.resolveExternalUri(resource, options); - - // TODO@Jo neither encodeURI nor toString(true) should be needed - // once we go with URL and not URI - return this._externalOpener.openExternal(encodeURI(resolved.toString(true))); } dispose() { diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 42ef1f6f542..02b337315c8 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -11,7 +11,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler'; import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { ITextAreaHandlerHelper, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; -import * as editorBrowser from 'vs/editor/browser/editorBrowser'; +import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; @@ -51,17 +51,15 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; export interface IContentWidgetData { - widget: editorBrowser.IContentWidget; - position: editorBrowser.IContentWidgetPosition | null; + widget: IContentWidget; + position: IContentWidgetPosition | null; } export interface IOverlayWidgetData { - widget: editorBrowser.IOverlayWidget; - position: editorBrowser.IOverlayWidgetPosition | null; + widget: IOverlayWidget; + position: IOverlayWidgetPosition | null; } -const invalidFunc = () => { throw new Error(`Invalid change accessor`); }; - export class View extends ViewEventHandler { private readonly eventDispatcher: ViewEventDispatcher; @@ -348,7 +346,7 @@ export class View extends ViewEventHandler { super.dispose(); } - private _renderOnce(callback: () => any): any { + private _renderOnce(callback: () => T): T { const r = safeInvokeNoArg(callback); this._scheduleRender(); return r; @@ -458,7 +456,7 @@ export class View extends ViewEventHandler { return visibleRange.left; } - public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget | null { + public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null { return this.pointerHandler.getTargetAtClientPoint(clientX, clientY); } @@ -466,42 +464,15 @@ export class View extends ViewEventHandler { return new OverviewRuler(this._context, cssClassName); } - public change(callback: (changeAccessor: editorBrowser.IViewZoneChangeAccessor) => any): boolean { - let zonesHaveChanged = false; - - this._renderOnce(() => { - const changeAccessor: editorBrowser.IViewZoneChangeAccessor = { - addZone: (zone: editorBrowser.IViewZone): string => { - zonesHaveChanged = true; - return this.viewZones.addZone(zone); - }, - removeZone: (id: string): void => { - if (!id) { - return; - } - zonesHaveChanged = this.viewZones.removeZone(id) || zonesHaveChanged; - }, - layoutZone: (id: string): void => { - if (!id) { - return; - } - zonesHaveChanged = this.viewZones.layoutZone(id) || zonesHaveChanged; - } - }; - - safeInvoke1Arg(callback, changeAccessor); - - // Invalidate changeAccessor - changeAccessor.addZone = invalidFunc; - changeAccessor.removeZone = invalidFunc; - changeAccessor.layoutZone = invalidFunc; - + public change(callback: (changeAccessor: IViewZoneChangeAccessor) => any): boolean { + return this._renderOnce(() => { + const zonesHaveChanged = this.viewZones.changeViewZones(callback); if (zonesHaveChanged) { this._context.viewLayout.onHeightMaybeChanged(); this._context.privateViewEventBus.emit(new viewEvents.ViewZonesChangedEvent()); } + return zonesHaveChanged; }); - return zonesHaveChanged; } public render(now: boolean, everything: boolean): void { @@ -582,10 +553,3 @@ function safeInvokeNoArg(func: Function): any { } } -function safeInvoke1Arg(func: Function, arg1: any): any { - try { - return func(arg1); - } catch (e) { - onUnexpectedError(e); - } -} diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index 494b7d7ca23..0cdd9c61cc2 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -177,7 +177,7 @@ export class GlyphMarginOverlay extends DedupOverlay { output[lineIndex] = ''; } else { output[lineIndex] = ( - '
; + public domNode: FastDomNode | null; public readonly input: RenderLineInput; protected readonly _characterMapping: CharacterMapping; @@ -420,7 +420,7 @@ class RenderedViewLine implements IRenderedViewLine { */ private readonly _pixelOffsetCache: Int32Array | null; - constructor(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType) { + constructor(domNode: FastDomNode | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType) { this.domNode = domNode; this.input = renderLineInput; this._characterMapping = characterMapping; @@ -439,16 +439,19 @@ class RenderedViewLine implements IRenderedViewLine { // --- Reading from the DOM methods - protected _getReadingTarget(): HTMLElement { - return this.domNode.domNode.firstChild; + protected _getReadingTarget(myDomNode: FastDomNode): HTMLElement { + return myDomNode.domNode.firstChild; } /** * Width of the line in pixels */ public getWidth(): number { + if (!this.domNode) { + return 0; + } if (this._cachedWidth === -1) { - this._cachedWidth = this._getReadingTarget().offsetWidth; + this._cachedWidth = this._getReadingTarget(this.domNode).offsetWidth; } return this._cachedWidth; } @@ -464,14 +467,17 @@ class RenderedViewLine implements IRenderedViewLine { * Visible ranges for a model range */ public getVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { + if (!this.domNode) { + return null; + } if (this._pixelOffsetCache !== null) { // the text is LTR - const startOffset = this._readPixelOffset(startColumn, context); + const startOffset = this._readPixelOffset(this.domNode, startColumn, context); if (startOffset === -1) { return null; } - const endOffset = this._readPixelOffset(endColumn, context); + const endOffset = this._readPixelOffset(this.domNode, endColumn, context); if (endOffset === -1) { return null; } @@ -479,23 +485,23 @@ class RenderedViewLine implements IRenderedViewLine { return [new HorizontalRange(startOffset, endOffset - startOffset)]; } - return this._readVisibleRangesForRange(startColumn, endColumn, context); + return this._readVisibleRangesForRange(this.domNode, startColumn, endColumn, context); } - protected _readVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { + protected _readVisibleRangesForRange(domNode: FastDomNode, startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { if (startColumn === endColumn) { - const pixelOffset = this._readPixelOffset(startColumn, context); + const pixelOffset = this._readPixelOffset(domNode, startColumn, context); if (pixelOffset === -1) { return null; } else { return [new HorizontalRange(pixelOffset, 0)]; } } else { - return this._readRawVisibleRangesForRange(startColumn, endColumn, context); + return this._readRawVisibleRangesForRange(domNode, startColumn, endColumn, context); } } - protected _readPixelOffset(column: number, context: DomReadingContext): number { + protected _readPixelOffset(domNode: FastDomNode, column: number, context: DomReadingContext): number { if (this._characterMapping.length === 0) { // This line has no content if (this._containsForeignElements === ForeignElementType.None) { @@ -520,18 +526,18 @@ class RenderedViewLine implements IRenderedViewLine { return cachedPixelOffset; } - const result = this._actualReadPixelOffset(column, context); + const result = this._actualReadPixelOffset(domNode, column, context); this._pixelOffsetCache[column] = result; return result; } - return this._actualReadPixelOffset(column, context); + return this._actualReadPixelOffset(domNode, column, context); } - private _actualReadPixelOffset(column: number, context: DomReadingContext): number { + private _actualReadPixelOffset(domNode: FastDomNode, column: number, context: DomReadingContext): number { if (this._characterMapping.length === 0) { // This line has no content - const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(), 0, 0, 0, 0, context.clientRectDeltaLeft, context.endNode); + const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), 0, 0, 0, 0, context.clientRectDeltaLeft, context.endNode); if (!r || r.length === 0) { return -1; } @@ -547,14 +553,14 @@ class RenderedViewLine implements IRenderedViewLine { const partIndex = CharacterMapping.getPartIndex(partData); const charOffsetInPart = CharacterMapping.getCharIndex(partData); - const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(), partIndex, charOffsetInPart, partIndex, charOffsetInPart, context.clientRectDeltaLeft, context.endNode); + const r = RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), partIndex, charOffsetInPart, partIndex, charOffsetInPart, context.clientRectDeltaLeft, context.endNode); if (!r || r.length === 0) { return -1; } return r[0].left; } - private _readRawVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { + private _readRawVisibleRangesForRange(domNode: FastDomNode, startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { if (startColumn === 1 && endColumn === this._characterMapping.length) { // This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line @@ -570,7 +576,7 @@ class RenderedViewLine implements IRenderedViewLine { const endPartIndex = CharacterMapping.getPartIndex(endPartData); const endCharOffsetInPart = CharacterMapping.getCharIndex(endPartData); - return RangeUtil.readHorizontalRanges(this._getReadingTarget(), startPartIndex, startCharOffsetInPart, endPartIndex, endCharOffsetInPart, context.clientRectDeltaLeft, context.endNode); + return RangeUtil.readHorizontalRanges(this._getReadingTarget(domNode), startPartIndex, startCharOffsetInPart, endPartIndex, endCharOffsetInPart, context.clientRectDeltaLeft, context.endNode); } /** @@ -591,8 +597,8 @@ class RenderedViewLine implements IRenderedViewLine { } class WebKitRenderedViewLine extends RenderedViewLine { - protected _readVisibleRangesForRange(startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { - const output = super._readVisibleRangesForRange(startColumn, endColumn, context); + protected _readVisibleRangesForRange(domNode: FastDomNode, startColumn: number, endColumn: number, context: DomReadingContext): HorizontalRange[] | null { + const output = super._readVisibleRangesForRange(domNode, startColumn, endColumn, context); if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) { return output; @@ -603,7 +609,7 @@ class WebKitRenderedViewLine extends RenderedViewLine { if (!this.input.containsRTL) { // This is an attempt to patch things up // Find position of last column - const endPixelOffset = this._readPixelOffset(endColumn, context); + const endPixelOffset = this._readPixelOffset(domNode, endColumn, context); if (endPixelOffset !== -1) { const lastRange = output[output.length - 1]; if (lastRange.left < endPixelOffset) { @@ -624,10 +630,10 @@ const createRenderedLine: (domNode: FastDomNode | null, renderLineI return createNormalRenderedLine; })(); -function createWebKitRenderedLine(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType): RenderedViewLine { +function createWebKitRenderedLine(domNode: FastDomNode | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType): RenderedViewLine { return new WebKitRenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements); } -function createNormalRenderedLine(domNode: FastDomNode, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType): RenderedViewLine { +function createNormalRenderedLine(domNode: FastDomNode | null, renderLineInput: RenderLineInput, characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType): RenderedViewLine { return new RenderedViewLine(domNode, renderLineInput, characterMapping, containsRTL, containsForeignElements); } diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 6d7e1473089..5e28b7785ec 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -5,7 +5,7 @@ import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IViewZone } from 'vs/editor/browser/editorBrowser'; +import { IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { Position } from 'vs/editor/common/core/position'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; @@ -13,7 +13,7 @@ import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewWhitespaceViewportData } from 'vs/editor/common/viewModel/viewModel'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; - +import { IWhitespaceChangeAccessor, IEditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; export interface IMyViewZone { whitespaceId: string; @@ -29,6 +29,8 @@ interface IComputedViewZoneProps { minWidthInPx: number; } +const invalidFunc = () => { throw new Error(`Invalid change accessor`); }; + export class ViewZones extends ViewPart { private _zones: { [id: string]: IMyViewZone; }; @@ -72,20 +74,29 @@ export class ViewZones extends ViewPart { // ---- begin view event handlers private _recomputeWhitespacesProps(): boolean { - let hadAChange = false; - - const keys = Object.keys(this._zones); - for (let i = 0, len = keys.length; i < len; i++) { - const id = keys[i]; - const zone = this._zones[id]; - const props = this._computeWhitespaceProps(zone.delegate); - if (this._context.viewLayout.changeWhitespace(id, props.afterViewLineNumber, props.heightInPx)) { - this._safeCallOnComputedHeight(zone.delegate, props.heightInPx); - hadAChange = true; - } + const whitespaces = this._context.viewLayout.getWhitespaces(); + const oldWhitespaces = new Map(); + for (const whitespace of whitespaces) { + oldWhitespaces.set(whitespace.id, whitespace); } + return this._context.viewLayout.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => { + let hadAChange = false; - return hadAChange; + const keys = Object.keys(this._zones); + for (let i = 0, len = keys.length; i < len; i++) { + const id = keys[i]; + const zone = this._zones[id]; + const props = this._computeWhitespaceProps(zone.delegate); + const oldWhitespace = oldWhitespaces.get(id); + if (oldWhitespace && (oldWhitespace.afterLineNumber !== props.afterViewLineNumber || oldWhitespace.height !== props.heightInPx)) { + whitespaceAccessor.changeOneWhitespace(id, props.afterViewLineNumber, props.heightInPx); + this._safeCallOnComputedHeight(zone.delegate, props.heightInPx); + hadAChange = true; + } + } + + return hadAChange; + }); } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { @@ -138,7 +149,6 @@ export class ViewZones extends ViewPart { return 10000; } - private _computeWhitespaceProps(zone: IViewZone): IComputedViewZoneProps { if (zone.afterLineNumber === 0) { return { @@ -188,9 +198,44 @@ export class ViewZones extends ViewPart { }; } - public addZone(zone: IViewZone): string { + public changeViewZones(callback: (changeAccessor: IViewZoneChangeAccessor) => any): boolean { + + return this._context.viewLayout.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => { + let zonesHaveChanged = false; + + const changeAccessor: IViewZoneChangeAccessor = { + addZone: (zone: IViewZone): string => { + zonesHaveChanged = true; + return this._addZone(whitespaceAccessor, zone); + }, + removeZone: (id: string): void => { + if (!id) { + return; + } + zonesHaveChanged = this._removeZone(whitespaceAccessor, id) || zonesHaveChanged; + }, + layoutZone: (id: string): void => { + if (!id) { + return; + } + zonesHaveChanged = this._layoutZone(whitespaceAccessor, id) || zonesHaveChanged; + } + }; + + safeInvoke1Arg(callback, changeAccessor); + + // Invalidate changeAccessor + changeAccessor.addZone = invalidFunc; + changeAccessor.removeZone = invalidFunc; + changeAccessor.layoutZone = invalidFunc; + + return zonesHaveChanged; + }); + } + + private _addZone(whitespaceAccessor: IWhitespaceChangeAccessor, zone: IViewZone): string { const props = this._computeWhitespaceProps(zone); - const whitespaceId = this._context.viewLayout.addWhitespace(props.afterViewLineNumber, this._getZoneOrdinal(zone), props.heightInPx, props.minWidthInPx); + const whitespaceId = whitespaceAccessor.insertWhitespace(props.afterViewLineNumber, this._getZoneOrdinal(zone), props.heightInPx, props.minWidthInPx); const myZone: IMyViewZone = { whitespaceId: whitespaceId, @@ -224,11 +269,11 @@ export class ViewZones extends ViewPart { return myZone.whitespaceId; } - public removeZone(id: string): boolean { + private _removeZone(whitespaceAccessor: IWhitespaceChangeAccessor, id: string): boolean { if (this._zones.hasOwnProperty(id)) { const zone = this._zones[id]; delete this._zones[id]; - this._context.viewLayout.removeWhitespace(zone.whitespaceId); + whitespaceAccessor.removeWhitespace(zone.whitespaceId); zone.domNode.removeAttribute('monaco-visible-view-zone'); zone.domNode.removeAttribute('monaco-view-zone'); @@ -247,21 +292,20 @@ export class ViewZones extends ViewPart { return false; } - public layoutZone(id: string): boolean { - let changed = false; + private _layoutZone(whitespaceAccessor: IWhitespaceChangeAccessor, id: string): boolean { if (this._zones.hasOwnProperty(id)) { const zone = this._zones[id]; const props = this._computeWhitespaceProps(zone.delegate); // const newOrdinal = this._getZoneOrdinal(zone.delegate); - changed = this._context.viewLayout.changeWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx) || changed; + whitespaceAccessor.changeOneWhitespace(zone.whitespaceId, props.afterViewLineNumber, props.heightInPx); // TODO@Alex: change `newOrdinal` too - if (changed) { - this._safeCallOnComputedHeight(zone.delegate, props.heightInPx); - this.setShouldRender(); - } + this._safeCallOnComputedHeight(zone.delegate, props.heightInPx); + this.setShouldRender(); + + return true; } - return changed; + return false; } public shouldSuppressMouseDownOnViewZone(id: string): boolean { @@ -365,3 +409,11 @@ export class ViewZones extends ViewPart { } } } + +function safeInvoke1Arg(func: Function, arg1: any): any { + try { + return func(arg1); + } catch (e) { + onUnexpectedError(e); + } +} diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 83e47a48f1d..357eeab2a9f 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -40,7 +40,7 @@ import * as modes from 'vs/editor/common/modes'; import { editorUnnecessaryCodeBorder, editorUnnecessaryCodeOpacity } from 'vs/editor/common/view/editorColorRegistry'; import { editorErrorBorder, editorErrorForeground, editorHintBorder, editorHintForeground, editorInfoBorder, editorInfoForeground, editorWarningBorder, editorWarningForeground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; -import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; +import { IEditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 0bd9f2788ff..9dbc302244e 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -32,7 +32,7 @@ import { IDiffComputationResult, IEditorWorkerService } from 'vs/editor/common/s import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; -import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; +import { IEditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; import { InlineDecoration, InlineDecorationType, ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -1376,12 +1376,16 @@ abstract class ViewZonesComputer { private readonly lineChanges: editorCommon.ILineChange[]; private readonly originalForeignVZ: IEditorWhitespace[]; + private readonly originalLineHeight: number; private readonly modifiedForeignVZ: IEditorWhitespace[]; + private readonly modifiedLineHeight: number; - constructor(lineChanges: editorCommon.ILineChange[], originalForeignVZ: IEditorWhitespace[], modifiedForeignVZ: IEditorWhitespace[]) { + constructor(lineChanges: editorCommon.ILineChange[], originalForeignVZ: IEditorWhitespace[], originalLineHeight: number, modifiedForeignVZ: IEditorWhitespace[], modifiedLineHeight: number) { this.lineChanges = lineChanges; this.originalForeignVZ = originalForeignVZ; + this.originalLineHeight = originalLineHeight; this.modifiedForeignVZ = modifiedForeignVZ; + this.modifiedLineHeight = modifiedLineHeight; } public getViewZones(): IEditorsZones { @@ -1456,7 +1460,7 @@ abstract class ViewZonesComputer { stepOriginal.push({ afterLineNumber: viewZoneLineNumber, - heightInLines: modifiedForeignVZ.current.heightInLines, + heightInLines: modifiedForeignVZ.current.height / this.modifiedLineHeight, domNode: null, marginDomNode: marginDomNode }); @@ -1473,7 +1477,7 @@ abstract class ViewZonesComputer { } stepModified.push({ afterLineNumber: viewZoneLineNumber, - heightInLines: originalForeignVZ.current.heightInLines, + heightInLines: originalForeignVZ.current.height / this.originalLineHeight, domNode: null }); originalForeignVZ.advance(); @@ -1732,7 +1736,7 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffE } protected _getViewZones(lineChanges: editorCommon.ILineChange[], originalForeignVZ: IEditorWhitespace[], modifiedForeignVZ: IEditorWhitespace[], originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor): IEditorsZones { - let c = new SideBySideViewZonesComputer(lineChanges, originalForeignVZ, modifiedForeignVZ); + let c = new SideBySideViewZonesComputer(lineChanges, originalForeignVZ, originalEditor.getOption(EditorOption.lineHeight), modifiedForeignVZ, modifiedEditor.getOption(EditorOption.lineHeight)); return c.getViewZones(); } @@ -1859,8 +1863,8 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffE class SideBySideViewZonesComputer extends ViewZonesComputer { - constructor(lineChanges: editorCommon.ILineChange[], originalForeignVZ: IEditorWhitespace[], modifiedForeignVZ: IEditorWhitespace[]) { - super(lineChanges, originalForeignVZ, modifiedForeignVZ); + constructor(lineChanges: editorCommon.ILineChange[], originalForeignVZ: IEditorWhitespace[], originalLineHeight: number, modifiedForeignVZ: IEditorWhitespace[], modifiedLineHeight: number) { + super(lineChanges, originalForeignVZ, originalLineHeight, modifiedForeignVZ, modifiedLineHeight); } protected _createOriginalMarginDomNodeForModifiedForeignViewZoneInAddedRegion(): HTMLDivElement | null { @@ -2020,7 +2024,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { private readonly renderIndicators: boolean; constructor(lineChanges: editorCommon.ILineChange[], originalForeignVZ: IEditorWhitespace[], modifiedForeignVZ: IEditorWhitespace[], originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor, renderIndicators: boolean) { - super(lineChanges, originalForeignVZ, modifiedForeignVZ); + super(lineChanges, originalForeignVZ, originalEditor.getOption(EditorOption.lineHeight), modifiedForeignVZ, modifiedEditor.getOption(EditorOption.lineHeight)); this.originalModel = originalEditor.getModel()!; this.modifiedEditorOptions = modifiedEditor.getOptions(); this.modifiedEditorTabSize = modifiedEditor.getModel()!.getOptions().tabSize; diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index d6673ac8b8f..90f7335de7a 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -4,44 +4,157 @@ *--------------------------------------------------------------------------------------------*/ import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import { IEditorWhitespace, WhitespaceComputer } from 'vs/editor/common/viewLayout/whitespaceComputer'; import { IViewWhitespaceViewportData } from 'vs/editor/common/viewModel/viewModel'; +import * as strings from 'vs/base/common/strings'; + +export interface IEditorWhitespace { + readonly id: string; + readonly afterLineNumber: number; + readonly height: number; +} + +/** + * An accessor that allows for whtiespace to be added, removed or changed in bulk. + */ +export interface IWhitespaceChangeAccessor { + insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string; + changeOneWhitespace(id: string, newAfterLineNumber: number, newHeight: number): void; + removeWhitespace(id: string): void; +} + +interface IPendingChange { id: string; newAfterLineNumber: number; newHeight: number; } +interface IPendingRemove { id: string; } + +class PendingChanges { + private _hasPending: boolean; + private _inserts: EditorWhitespace[]; + private _changes: IPendingChange[]; + private _removes: IPendingRemove[]; + + constructor() { + this._hasPending = false; + this._inserts = []; + this._changes = []; + this._removes = []; + } + + public insert(x: EditorWhitespace): void { + this._hasPending = true; + this._inserts.push(x); + } + + public change(x: IPendingChange): void { + this._hasPending = true; + this._changes.push(x); + } + + public remove(x: IPendingRemove): void { + this._hasPending = true; + this._removes.push(x); + } + + public mustCommit(): boolean { + return this._hasPending; + } + + public commit(linesLayout: LinesLayout): void { + if (!this._hasPending) { + return; + } + + const inserts = this._inserts; + const changes = this._changes; + const removes = this._removes; + + this._hasPending = false; + this._inserts = []; + this._changes = []; + this._removes = []; + + linesLayout._commitPendingChanges(inserts, changes, removes); + } +} + +export class EditorWhitespace implements IEditorWhitespace { + public id: string; + public afterLineNumber: number; + public ordinal: number; + public height: number; + public minWidth: number; + public prefixSum: number; + + constructor(id: string, afterLineNumber: number, ordinal: number, height: number, minWidth: number) { + this.id = id; + this.afterLineNumber = afterLineNumber; + this.ordinal = ordinal; + this.height = height; + this.minWidth = minWidth; + this.prefixSum = 0; + } +} /** * Layouting of objects that take vertical space (by having a height) and push down other objects. * * These objects are basically either text (lines) or spaces between those lines (whitespaces). * This provides commodity operations for working with lines that contain whitespace that pushes lines lower (vertically). - * This is written with no knowledge of an editor in mind. */ export class LinesLayout { - /** - * Keep track of the total number of lines. - * This is useful for doing binary searches or for doing hit-testing. - */ - private _lineCount: number; + private static INSTANCE_COUNT = 0; - /** - * The height of a line in pixels. - */ + private readonly _instanceId: string; + private readonly _pendingChanges: PendingChanges; + private _lastWhitespaceId: number; + private _arr: EditorWhitespace[]; + private _prefixSumValidIndex: number; + private _minWidth: number; + private _lineCount: number; private _lineHeight: number; - /** - * Contains whitespace information in pixels - */ - private readonly _whitespaces: WhitespaceComputer; - constructor(lineCount: number, lineHeight: number) { + this._instanceId = strings.singleLetterHash(++LinesLayout.INSTANCE_COUNT); + this._pendingChanges = new PendingChanges(); + this._lastWhitespaceId = 0; + this._arr = []; + this._prefixSumValidIndex = -1; + this._minWidth = -1; /* marker for not being computed */ this._lineCount = lineCount; this._lineHeight = lineHeight; - this._whitespaces = new WhitespaceComputer(); + } + + /** + * Find the insertion index for a new value inside a sorted array of values. + * If the value is already present in the sorted array, the insertion index will be after the already existing value. + */ + public static findInsertionIndex(arr: EditorWhitespace[], afterLineNumber: number, ordinal: number): number { + let low = 0; + let high = arr.length; + + while (low < high) { + const mid = ((low + high) >>> 1); + + if (afterLineNumber === arr[mid].afterLineNumber) { + if (ordinal < arr[mid].ordinal) { + high = mid; + } else { + low = mid + 1; + } + } else if (afterLineNumber < arr[mid].afterLineNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return low; } /** * Change the height of a line in pixels. */ public setLineHeight(lineHeight: number): void { + this._checkPendingChanges(); this._lineHeight = lineHeight; } @@ -51,37 +164,153 @@ export class LinesLayout { * @param lineCount New number of lines. */ public onFlushed(lineCount: number): void { + this._checkPendingChanges(); this._lineCount = lineCount; } - /** - * Insert a new whitespace of a certain height after a line number. - * The whitespace has a "sticky" characteristic. - * Irrespective of edits above or below `afterLineNumber`, the whitespace will follow the initial line. - * - * @param afterLineNumber The conceptual position of this whitespace. The whitespace will follow this line as best as possible even when deleting/inserting lines above/below. - * @param heightInPx The height of the whitespace, in pixels. - * @return An id that can be used later to mutate or delete the whitespace - */ - public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string { - return this._whitespaces.insertWhitespace(afterLineNumber, ordinal, heightInPx, minWidth); + public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => T): T { + try { + const accessor = { + insertWhitespace: (afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string => { + afterLineNumber = afterLineNumber | 0; + ordinal = ordinal | 0; + heightInPx = heightInPx | 0; + minWidth = minWidth | 0; + + const id = this._instanceId + (++this._lastWhitespaceId); + this._pendingChanges.insert(new EditorWhitespace(id, afterLineNumber, ordinal, heightInPx, minWidth)); + return id; + }, + changeOneWhitespace: (id: string, newAfterLineNumber: number, newHeight: number): void => { + newAfterLineNumber = newAfterLineNumber | 0; + newHeight = newHeight | 0; + + this._pendingChanges.change({ id, newAfterLineNumber, newHeight }); + }, + removeWhitespace: (id: string): void => { + this._pendingChanges.remove({ id }); + } + }; + return callback(accessor); + } finally { + this._pendingChanges.commit(this); + } } - /** - * Change properties associated with a certain whitespace. - */ - public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { - return this._whitespaces.changeWhitespace(id, newAfterLineNumber, newHeight); + public _commitPendingChanges(inserts: EditorWhitespace[], changes: IPendingChange[], removes: IPendingRemove[]): void { + if (inserts.length > 0 || removes.length > 0) { + this._minWidth = -1; /* marker for not being computed */ + } + + if (inserts.length + changes.length + removes.length <= 1) { + // when only one thing happened, handle it "delicately" + for (const insert of inserts) { + this._insertWhitespace(insert); + } + for (const change of changes) { + this._changeOneWhitespace(change.id, change.newAfterLineNumber, change.newHeight); + } + for (const remove of removes) { + const index = this._findWhitespaceIndex(remove.id); + if (index === -1) { + continue; + } + this._removeWhitespace(index); + } + return; + } + + // simply rebuild the entire datastructure + + const toRemove = new Set(); + for (const remove of removes) { + toRemove.add(remove.id); + } + + const toChange = new Map(); + for (const change of changes) { + toChange.set(change.id, change); + } + + const applyRemoveAndChange = (whitespaces: EditorWhitespace[]): EditorWhitespace[] => { + let result: EditorWhitespace[] = []; + for (const whitespace of whitespaces) { + if (toRemove.has(whitespace.id)) { + continue; + } + if (toChange.has(whitespace.id)) { + const change = toChange.get(whitespace.id)!; + whitespace.afterLineNumber = change.newAfterLineNumber; + whitespace.height = change.newHeight; + } + result.push(whitespace); + } + return result; + }; + + const result = applyRemoveAndChange(this._arr).concat(applyRemoveAndChange(inserts)); + result.sort((a, b) => { + if (a.afterLineNumber === b.afterLineNumber) { + return a.ordinal - b.ordinal; + } + return a.afterLineNumber - b.afterLineNumber; + }); + + this._arr = result; + this._prefixSumValidIndex = -1; } - /** - * Remove an existing whitespace. - * - * @param id The whitespace to remove - * @return Returns true if the whitespace is found and it is removed. - */ - public removeWhitespace(id: string): boolean { - return this._whitespaces.removeWhitespace(id); + private _checkPendingChanges(): void { + if (this._pendingChanges.mustCommit()) { + console.warn(`Commiting pending changes before change accessor leaves due to read access.`); + this._pendingChanges.commit(this); + } + } + + private _insertWhitespace(whitespace: EditorWhitespace): void { + const insertIndex = LinesLayout.findInsertionIndex(this._arr, whitespace.afterLineNumber, whitespace.ordinal); + this._arr.splice(insertIndex, 0, whitespace); + this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, insertIndex - 1); + } + + private _findWhitespaceIndex(id: string): number { + const arr = this._arr; + for (let i = 0, len = arr.length; i < len; i++) { + if (arr[i].id === id) { + return i; + } + } + return -1; + } + + private _changeOneWhitespace(id: string, newAfterLineNumber: number, newHeight: number): void { + const index = this._findWhitespaceIndex(id); + if (index === -1) { + return; + } + if (this._arr[index].height !== newHeight) { + this._arr[index].height = newHeight; + this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, index - 1); + } + if (this._arr[index].afterLineNumber !== newAfterLineNumber) { + // `afterLineNumber` changed for this whitespace + + // Record old whitespace + const whitespace = this._arr[index]; + + // Since changing `afterLineNumber` can trigger a reordering, we're gonna remove this whitespace + this._removeWhitespace(index); + + whitespace.afterLineNumber = newAfterLineNumber; + + // And add it again + this._insertWhitespace(whitespace); + } + } + + private _removeWhitespace(removeIndex: number): void { + this._arr.splice(removeIndex, 1); + this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, removeIndex - 1); } /** @@ -91,8 +320,24 @@ export class LinesLayout { * @param toLineNumber The line number at which the deletion ended, inclusive */ public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { + this._checkPendingChanges(); + fromLineNumber = fromLineNumber | 0; + toLineNumber = toLineNumber | 0; + this._lineCount -= (toLineNumber - fromLineNumber + 1); - this._whitespaces.onLinesDeleted(fromLineNumber, toLineNumber); + for (let i = 0, len = this._arr.length; i < len; i++) { + const afterLineNumber = this._arr[i].afterLineNumber; + + if (fromLineNumber <= afterLineNumber && afterLineNumber <= toLineNumber) { + // The line this whitespace was after has been deleted + // => move whitespace to before first deleted line + this._arr[i].afterLineNumber = fromLineNumber - 1; + } else if (afterLineNumber > toLineNumber) { + // The line this whitespace was after has been moved up + // => move whitespace up + this._arr[i].afterLineNumber -= (toLineNumber - fromLineNumber + 1); + } + } } /** @@ -102,8 +347,53 @@ export class LinesLayout { * @param toLineNumber The line number at which the insertion ended, inclusive. */ public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + this._checkPendingChanges(); + fromLineNumber = fromLineNumber | 0; + toLineNumber = toLineNumber | 0; + this._lineCount += (toLineNumber - fromLineNumber + 1); - this._whitespaces.onLinesInserted(fromLineNumber, toLineNumber); + for (let i = 0, len = this._arr.length; i < len; i++) { + const afterLineNumber = this._arr[i].afterLineNumber; + + if (fromLineNumber <= afterLineNumber) { + this._arr[i].afterLineNumber += (toLineNumber - fromLineNumber + 1); + } + } + } + + /** + * Get the sum of all the whitespaces. + */ + public getWhitespacesTotalHeight(): number { + this._checkPendingChanges(); + if (this._arr.length === 0) { + return 0; + } + return this.getWhitespacesAccumulatedHeight(this._arr.length - 1); + } + + /** + * Return the sum of the heights of the whitespaces at [0..index]. + * This includes the whitespace at `index`. + * + * @param index The index of the whitespace. + * @return The sum of the heights of all whitespaces before the one at `index`, including the one at `index`. + */ + public getWhitespacesAccumulatedHeight(index: number): number { + this._checkPendingChanges(); + index = index | 0; + + let startIndex = Math.max(0, this._prefixSumValidIndex + 1); + if (startIndex === 0) { + this._arr[0].prefixSum = this._arr[0].height; + startIndex++; + } + + for (let i = startIndex; i <= index; i++) { + this._arr[i].prefixSum = this._arr[i - 1].prefixSum + this._arr[i].height; + } + this._prefixSumValidIndex = Math.max(this._prefixSumValidIndex, index); + return this._arr[index].prefixSum; } /** @@ -112,11 +402,81 @@ export class LinesLayout { * @return The sum of heights for all objects. */ public getLinesTotalHeight(): number { - let linesHeight = this._lineHeight * this._lineCount; - let whitespacesHeight = this._whitespaces.getTotalHeight(); + this._checkPendingChanges(); + const linesHeight = this._lineHeight * this._lineCount; + const whitespacesHeight = this.getWhitespacesTotalHeight(); return linesHeight + whitespacesHeight; } + /** + * Returns the accumulated height of whitespaces before the given line number. + * + * @param lineNumber The line number + */ + public getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber: number): number { + this._checkPendingChanges(); + lineNumber = lineNumber | 0; + + const lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeLineNumber(lineNumber); + + if (lastWhitespaceBeforeLineNumber === -1) { + return 0; + } + + return this.getWhitespacesAccumulatedHeight(lastWhitespaceBeforeLineNumber); + } + + private _findLastWhitespaceBeforeLineNumber(lineNumber: number): number { + lineNumber = lineNumber | 0; + + // Find the whitespace before line number + const arr = this._arr; + let low = 0; + let high = arr.length - 1; + + while (low <= high) { + const delta = (high - low) | 0; + const halfDelta = (delta / 2) | 0; + const mid = (low + halfDelta) | 0; + + if (arr[mid].afterLineNumber < lineNumber) { + if (mid + 1 >= arr.length || arr[mid + 1].afterLineNumber >= lineNumber) { + return mid; + } else { + low = (mid + 1) | 0; + } + } else { + high = (mid - 1) | 0; + } + } + + return -1; + } + + private _findFirstWhitespaceAfterLineNumber(lineNumber: number): number { + lineNumber = lineNumber | 0; + + const lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeLineNumber(lineNumber); + const firstWhitespaceAfterLineNumber = lastWhitespaceBeforeLineNumber + 1; + + if (firstWhitespaceAfterLineNumber < this._arr.length) { + return firstWhitespaceAfterLineNumber; + } + + return -1; + } + + /** + * Find the index of the first whitespace which has `afterLineNumber` >= `lineNumber`. + * @return The index of the first whitespace with `afterLineNumber` >= `lineNumber` or -1 if no whitespace is found. + */ + public getFirstWhitespaceIndexAfterLineNumber(lineNumber: number): number { + this._checkPendingChanges(); + lineNumber = lineNumber | 0; + + return this._findFirstWhitespaceAfterLineNumber(lineNumber); + } + /** * Get the vertical offset (the sum of heights for all objects above) a certain line number. * @@ -124,6 +484,7 @@ export class LinesLayout { * @return The sum of heights for all objects above `lineNumber`. */ public getVerticalOffsetForLineNumber(lineNumber: number): number { + this._checkPendingChanges(); lineNumber = lineNumber | 0; let previousLinesHeight: number; @@ -133,36 +494,40 @@ export class LinesLayout { previousLinesHeight = 0; } - let previousWhitespacesHeight = this._whitespaces.getAccumulatedHeightBeforeLineNumber(lineNumber); + const previousWhitespacesHeight = this.getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber); return previousLinesHeight + previousWhitespacesHeight; } - /** - * Returns the accumulated height of whitespaces before the given line number. - * - * @param lineNumber The line number - */ - public getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber: number): number { - return this._whitespaces.getAccumulatedHeightBeforeLineNumber(lineNumber); - } - /** * Returns if there is any whitespace in the document. */ public hasWhitespace(): boolean { - return this._whitespaces.getCount() > 0; + this._checkPendingChanges(); + return this.getWhitespacesCount() > 0; } + /** + * The maximum min width for all whitespaces. + */ public getWhitespaceMinWidth(): number { - return this._whitespaces.getMinWidth(); + this._checkPendingChanges(); + if (this._minWidth === -1) { + let minWidth = 0; + for (let i = 0, len = this._arr.length; i < len; i++) { + minWidth = Math.max(minWidth, this._arr[i].minWidth); + } + this._minWidth = minWidth; + } + return this._minWidth; } /** * Check if `verticalOffset` is below all lines. */ public isAfterLines(verticalOffset: number): boolean { - let totalHeight = this.getLinesTotalHeight(); + this._checkPendingChanges(); + const totalHeight = this.getLinesTotalHeight(); return verticalOffset > totalHeight; } @@ -175,6 +540,7 @@ export class LinesLayout { * @return The line number at or after vertical offset `verticalOffset`. */ public getLineNumberAtOrAfterVerticalOffset(verticalOffset: number): number { + this._checkPendingChanges(); verticalOffset = verticalOffset | 0; if (verticalOffset < 0) { @@ -187,9 +553,9 @@ export class LinesLayout { let maxLineNumber = linesCount; while (minLineNumber < maxLineNumber) { - let midLineNumber = ((minLineNumber + maxLineNumber) / 2) | 0; + const midLineNumber = ((minLineNumber + maxLineNumber) / 2) | 0; - let midLineNumberVerticalOffset = this.getVerticalOffsetForLineNumber(midLineNumber) | 0; + const midLineNumberVerticalOffset = this.getVerticalOffsetForLineNumber(midLineNumber) | 0; if (verticalOffset >= midLineNumberVerticalOffset + lineHeight) { // vertical offset is after mid line number @@ -218,6 +584,7 @@ export class LinesLayout { * @return A structure describing the lines positioned between `verticalOffset1` and `verticalOffset2`. */ public getLinesViewportData(verticalOffset1: number, verticalOffset2: number): IPartialViewLinesViewportData { + this._checkPendingChanges(); verticalOffset1 = verticalOffset1 | 0; verticalOffset2 = verticalOffset2 | 0; const lineHeight = this._lineHeight; @@ -230,8 +597,8 @@ export class LinesLayout { let endLineNumber = this._lineCount | 0; // Also keep track of what whitespace we've got - let whitespaceIndex = this._whitespaces.getFirstWhitespaceIndexAfterLineNumber(startLineNumber) | 0; - const whitespaceCount = this._whitespaces.getCount() | 0; + let whitespaceIndex = this.getFirstWhitespaceIndexAfterLineNumber(startLineNumber) | 0; + const whitespaceCount = this.getWhitespacesCount() | 0; let currentWhitespaceHeight: number; let currentWhitespaceAfterLineNumber: number; @@ -240,8 +607,8 @@ export class LinesLayout { currentWhitespaceAfterLineNumber = endLineNumber + 1; currentWhitespaceHeight = 0; } else { - currentWhitespaceAfterLineNumber = this._whitespaces.getAfterLineNumberForWhitespaceIndex(whitespaceIndex) | 0; - currentWhitespaceHeight = this._whitespaces.getHeightForWhitespaceIndex(whitespaceIndex) | 0; + currentWhitespaceAfterLineNumber = this.getAfterLineNumberForWhitespaceIndex(whitespaceIndex) | 0; + currentWhitespaceHeight = this.getHeightForWhitespaceIndex(whitespaceIndex) | 0; } let currentVerticalOffset = startLineNumberVerticalOffset; @@ -258,7 +625,7 @@ export class LinesLayout { currentLineRelativeOffset -= bigNumbersDelta; } - let linesOffsets: number[] = []; + const linesOffsets: number[] = []; const verticalCenter = verticalOffset1 + (verticalOffset2 - verticalOffset1) / 2; let centeredLineNumber = -1; @@ -267,8 +634,8 @@ export class LinesLayout { for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { if (centeredLineNumber === -1) { - let currentLineTop = currentVerticalOffset; - let currentLineBottom = currentVerticalOffset + lineHeight; + const currentLineTop = currentVerticalOffset; + const currentLineBottom = currentVerticalOffset + lineHeight; if ((currentLineTop <= verticalCenter && verticalCenter < currentLineBottom) || currentLineTop > verticalCenter) { centeredLineNumber = lineNumber; } @@ -291,8 +658,8 @@ export class LinesLayout { if (whitespaceIndex >= whitespaceCount) { currentWhitespaceAfterLineNumber = endLineNumber + 1; } else { - currentWhitespaceAfterLineNumber = this._whitespaces.getAfterLineNumberForWhitespaceIndex(whitespaceIndex) | 0; - currentWhitespaceHeight = this._whitespaces.getHeightForWhitespaceIndex(whitespaceIndex) | 0; + currentWhitespaceAfterLineNumber = this.getAfterLineNumberForWhitespaceIndex(whitespaceIndex) | 0; + currentWhitespaceHeight = this.getHeightForWhitespaceIndex(whitespaceIndex) | 0; } } @@ -335,9 +702,10 @@ export class LinesLayout { } public getVerticalOffsetForWhitespaceIndex(whitespaceIndex: number): number { + this._checkPendingChanges(); whitespaceIndex = whitespaceIndex | 0; - let afterLineNumber = this._whitespaces.getAfterLineNumberForWhitespaceIndex(whitespaceIndex); + const afterLineNumber = this.getAfterLineNumberForWhitespaceIndex(whitespaceIndex); let previousLinesHeight: number; if (afterLineNumber >= 1) { @@ -348,7 +716,7 @@ export class LinesLayout { let previousWhitespacesHeight: number; if (whitespaceIndex > 0) { - previousWhitespacesHeight = this._whitespaces.getAccumulatedHeight(whitespaceIndex - 1); + previousWhitespacesHeight = this.getWhitespacesAccumulatedHeight(whitespaceIndex - 1); } else { previousWhitespacesHeight = 0; } @@ -356,30 +724,28 @@ export class LinesLayout { } public getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset: number): number { + this._checkPendingChanges(); verticalOffset = verticalOffset | 0; - let midWhitespaceIndex: number, - minWhitespaceIndex = 0, - maxWhitespaceIndex = this._whitespaces.getCount() - 1, - midWhitespaceVerticalOffset: number, - midWhitespaceHeight: number; + let minWhitespaceIndex = 0; + let maxWhitespaceIndex = this.getWhitespacesCount() - 1; if (maxWhitespaceIndex < 0) { return -1; } // Special case: nothing to be found - let maxWhitespaceVerticalOffset = this.getVerticalOffsetForWhitespaceIndex(maxWhitespaceIndex); - let maxWhitespaceHeight = this._whitespaces.getHeightForWhitespaceIndex(maxWhitespaceIndex); + const maxWhitespaceVerticalOffset = this.getVerticalOffsetForWhitespaceIndex(maxWhitespaceIndex); + const maxWhitespaceHeight = this.getHeightForWhitespaceIndex(maxWhitespaceIndex); if (verticalOffset >= maxWhitespaceVerticalOffset + maxWhitespaceHeight) { return -1; } while (minWhitespaceIndex < maxWhitespaceIndex) { - midWhitespaceIndex = Math.floor((minWhitespaceIndex + maxWhitespaceIndex) / 2); + const midWhitespaceIndex = Math.floor((minWhitespaceIndex + maxWhitespaceIndex) / 2); - midWhitespaceVerticalOffset = this.getVerticalOffsetForWhitespaceIndex(midWhitespaceIndex); - midWhitespaceHeight = this._whitespaces.getHeightForWhitespaceIndex(midWhitespaceIndex); + const midWhitespaceVerticalOffset = this.getVerticalOffsetForWhitespaceIndex(midWhitespaceIndex); + const midWhitespaceHeight = this.getHeightForWhitespaceIndex(midWhitespaceIndex); if (verticalOffset >= midWhitespaceVerticalOffset + midWhitespaceHeight) { // vertical offset is after whitespace @@ -402,27 +768,28 @@ export class LinesLayout { * @return Precisely the whitespace that is layouted at `verticaloffset` or null. */ public getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null { + this._checkPendingChanges(); verticalOffset = verticalOffset | 0; - let candidateIndex = this.getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset); + const candidateIndex = this.getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset); if (candidateIndex < 0) { return null; } - if (candidateIndex >= this._whitespaces.getCount()) { + if (candidateIndex >= this.getWhitespacesCount()) { return null; } - let candidateTop = this.getVerticalOffsetForWhitespaceIndex(candidateIndex); + const candidateTop = this.getVerticalOffsetForWhitespaceIndex(candidateIndex); if (candidateTop > verticalOffset) { return null; } - let candidateHeight = this._whitespaces.getHeightForWhitespaceIndex(candidateIndex); - let candidateId = this._whitespaces.getIdForWhitespaceIndex(candidateIndex); - let candidateAfterLineNumber = this._whitespaces.getAfterLineNumberForWhitespaceIndex(candidateIndex); + const candidateHeight = this.getHeightForWhitespaceIndex(candidateIndex); + const candidateId = this.getIdForWhitespaceIndex(candidateIndex); + const candidateAfterLineNumber = this.getAfterLineNumberForWhitespaceIndex(candidateIndex); return { id: candidateId, @@ -440,11 +807,12 @@ export class LinesLayout { * @return An array with all the whitespaces in the viewport. If no whitespace is in viewport, the array is empty. */ public getWhitespaceViewportData(verticalOffset1: number, verticalOffset2: number): IViewWhitespaceViewportData[] { + this._checkPendingChanges(); verticalOffset1 = verticalOffset1 | 0; verticalOffset2 = verticalOffset2 | 0; - let startIndex = this.getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset1); - let endIndex = this._whitespaces.getCount() - 1; + const startIndex = this.getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset1); + const endIndex = this.getWhitespacesCount() - 1; if (startIndex < 0) { return []; @@ -452,15 +820,15 @@ export class LinesLayout { let result: IViewWhitespaceViewportData[] = []; for (let i = startIndex; i <= endIndex; i++) { - let top = this.getVerticalOffsetForWhitespaceIndex(i); - let height = this._whitespaces.getHeightForWhitespaceIndex(i); + const top = this.getVerticalOffsetForWhitespaceIndex(i); + const height = this.getHeightForWhitespaceIndex(i); if (top >= verticalOffset2) { break; } result.push({ - id: this._whitespaces.getIdForWhitespaceIndex(i), - afterLineNumber: this._whitespaces.getAfterLineNumberForWhitespaceIndex(i), + id: this.getIdForWhitespaceIndex(i), + afterLineNumber: this.getAfterLineNumberForWhitespaceIndex(i), verticalOffset: top, height: height }); @@ -473,6 +841,54 @@ export class LinesLayout { * Get all whitespaces. */ public getWhitespaces(): IEditorWhitespace[] { - return this._whitespaces.getWhitespaces(this._lineHeight); + this._checkPendingChanges(); + return this._arr.slice(0); + } + + /** + * The number of whitespaces. + */ + public getWhitespacesCount(): number { + this._checkPendingChanges(); + return this._arr.length; + } + + /** + * Get the `id` for whitespace at index `index`. + * + * @param index The index of the whitespace. + * @return `id` of whitespace at `index`. + */ + public getIdForWhitespaceIndex(index: number): string { + this._checkPendingChanges(); + index = index | 0; + + return this._arr[index].id; + } + + /** + * Get the `afterLineNumber` for whitespace at index `index`. + * + * @param index The index of the whitespace. + * @return `afterLineNumber` of whitespace at `index`. + */ + public getAfterLineNumberForWhitespaceIndex(index: number): number { + this._checkPendingChanges(); + index = index | 0; + + return this._arr[index].afterLineNumber; + } + + /** + * Get the `height` for whitespace at index `index`. + * + * @param index The index of the whitespace. + * @return `height` of whitespace at `index`. + */ + public getHeightForWhitespaceIndex(index: number): number { + this._checkPendingChanges(); + index = index | 0; + + return this._arr[index].height; } } diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 74fca28d029..115e4a1c85b 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -8,9 +8,8 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { LinesLayout } from 'vs/editor/common/viewLayout/linesLayout'; +import { LinesLayout, IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; import { IViewLayout, IViewWhitespaceViewportData, Viewport } from 'vs/editor/common/viewModel/viewModel'; const SMOOTH_SCROLLING_TIME = 125; @@ -194,15 +193,8 @@ export class ViewLayout extends Disposable implements IViewLayout { } // ---- IVerticalLayoutProvider - - public addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): string { - return this._linesLayout.insertWhitespace(afterLineNumber, ordinal, height, minWidth); - } - public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { - return this._linesLayout.changeWhitespace(id, newAfterLineNumber, newHeight); - } - public removeWhitespace(id: string): boolean { - return this._linesLayout.removeWhitespace(id); + public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => T): T { + return this._linesLayout.changeWhitespace(callback); } public getVerticalOffsetForLineNumber(lineNumber: number): number { return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber); diff --git a/src/vs/editor/common/viewLayout/whitespaceComputer.ts b/src/vs/editor/common/viewLayout/whitespaceComputer.ts deleted file mode 100644 index 8e5dd347e83..00000000000 --- a/src/vs/editor/common/viewLayout/whitespaceComputer.ts +++ /dev/null @@ -1,493 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as strings from 'vs/base/common/strings'; - -export interface IEditorWhitespace { - readonly id: string; - readonly afterLineNumber: number; - readonly heightInLines: number; -} - -/** - * Represent whitespaces in between lines and provide fast CRUD management methods. - * The whitespaces are sorted ascending by `afterLineNumber`. - */ -export class WhitespaceComputer { - - private static INSTANCE_COUNT = 0; - - private readonly _instanceId: string; - - /** - * heights[i] is the height in pixels for whitespace at index i - */ - private readonly _heights: number[]; - - /** - * minWidths[i] is the min width in pixels for whitespace at index i - */ - private readonly _minWidths: number[]; - - /** - * afterLineNumbers[i] is the line number whitespace at index i is after - */ - private readonly _afterLineNumbers: number[]; - - /** - * ordinals[i] is the orinal of the whitespace at index i - */ - private readonly _ordinals: number[]; - - /** - * prefixSum[i] = SUM(heights[j]), 1 <= j <= i - */ - private readonly _prefixSum: number[]; - - /** - * prefixSum[i], 1 <= i <= prefixSumValidIndex can be trusted - */ - private _prefixSumValidIndex: number; - - /** - * ids[i] is the whitespace id of whitespace at index i - */ - private readonly _ids: string[]; - - /** - * index at which a whitespace is positioned (inside heights, afterLineNumbers, prefixSum members) - */ - private readonly _whitespaceId2Index: { - [id: string]: number; - }; - - /** - * last whitespace id issued - */ - private _lastWhitespaceId: number; - - private _minWidth: number; - - constructor() { - this._instanceId = strings.singleLetterHash(++WhitespaceComputer.INSTANCE_COUNT); - this._heights = []; - this._minWidths = []; - this._ids = []; - this._afterLineNumbers = []; - this._ordinals = []; - this._prefixSum = []; - this._prefixSumValidIndex = -1; - this._whitespaceId2Index = {}; - this._lastWhitespaceId = 0; - this._minWidth = -1; /* marker for not being computed */ - } - - /** - * Find the insertion index for a new value inside a sorted array of values. - * If the value is already present in the sorted array, the insertion index will be after the already existing value. - */ - public static findInsertionIndex(sortedArray: number[], value: number, ordinals: number[], valueOrdinal: number): number { - let low = 0; - let high = sortedArray.length; - - while (low < high) { - let mid = ((low + high) >>> 1); - - if (value === sortedArray[mid]) { - if (valueOrdinal < ordinals[mid]) { - high = mid; - } else { - low = mid + 1; - } - } else if (value < sortedArray[mid]) { - high = mid; - } else { - low = mid + 1; - } - } - - return low; - } - - /** - * Insert a new whitespace of a certain height after a line number. - * The whitespace has a "sticky" characteristic. - * Irrespective of edits above or below `afterLineNumber`, the whitespace will follow the initial line. - * - * @param afterLineNumber The conceptual position of this whitespace. The whitespace will follow this line as best as possible even when deleting/inserting lines above/below. - * @param heightInPx The height of the whitespace, in pixels. - * @return An id that can be used later to mutate or delete the whitespace - */ - public insertWhitespace(afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string { - afterLineNumber = afterLineNumber | 0; - ordinal = ordinal | 0; - heightInPx = heightInPx | 0; - minWidth = minWidth | 0; - - let id = this._instanceId + (++this._lastWhitespaceId); - let insertionIndex = WhitespaceComputer.findInsertionIndex(this._afterLineNumbers, afterLineNumber, this._ordinals, ordinal); - this._insertWhitespaceAtIndex(id, insertionIndex, afterLineNumber, ordinal, heightInPx, minWidth); - this._minWidth = -1; /* marker for not being computed */ - return id; - } - - private _insertWhitespaceAtIndex(id: string, insertIndex: number, afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): void { - insertIndex = insertIndex | 0; - afterLineNumber = afterLineNumber | 0; - ordinal = ordinal | 0; - heightInPx = heightInPx | 0; - minWidth = minWidth | 0; - - this._heights.splice(insertIndex, 0, heightInPx); - this._minWidths.splice(insertIndex, 0, minWidth); - this._ids.splice(insertIndex, 0, id); - this._afterLineNumbers.splice(insertIndex, 0, afterLineNumber); - this._ordinals.splice(insertIndex, 0, ordinal); - this._prefixSum.splice(insertIndex, 0, 0); - - let keys = Object.keys(this._whitespaceId2Index); - for (let i = 0, len = keys.length; i < len; i++) { - let sid = keys[i]; - let oldIndex = this._whitespaceId2Index[sid]; - if (oldIndex >= insertIndex) { - this._whitespaceId2Index[sid] = oldIndex + 1; - } - } - - this._whitespaceId2Index[id] = insertIndex; - this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, insertIndex - 1); - } - - /** - * Change properties associated with a certain whitespace. - */ - public changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean { - newAfterLineNumber = newAfterLineNumber | 0; - newHeight = newHeight | 0; - - let hasChanges = false; - hasChanges = this.changeWhitespaceHeight(id, newHeight) || hasChanges; - hasChanges = this.changeWhitespaceAfterLineNumber(id, newAfterLineNumber) || hasChanges; - return hasChanges; - } - - /** - * Change the height of an existing whitespace - * - * @param id The whitespace to change - * @param newHeightInPx The new height of the whitespace, in pixels - * @return Returns true if the whitespace is found and if the new height is different than the old height - */ - public changeWhitespaceHeight(id: string, newHeightInPx: number): boolean { - newHeightInPx = newHeightInPx | 0; - - if (this._whitespaceId2Index.hasOwnProperty(id)) { - let index = this._whitespaceId2Index[id]; - if (this._heights[index] !== newHeightInPx) { - this._heights[index] = newHeightInPx; - this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, index - 1); - return true; - } - } - return false; - } - - /** - * Change the line number after which an existing whitespace flows. - * - * @param id The whitespace to change - * @param newAfterLineNumber The new line number the whitespace will follow - * @return Returns true if the whitespace is found and if the new line number is different than the old line number - */ - public changeWhitespaceAfterLineNumber(id: string, newAfterLineNumber: number): boolean { - newAfterLineNumber = newAfterLineNumber | 0; - - if (this._whitespaceId2Index.hasOwnProperty(id)) { - let index = this._whitespaceId2Index[id]; - if (this._afterLineNumbers[index] !== newAfterLineNumber) { - // `afterLineNumber` changed for this whitespace - - // Record old ordinal - let ordinal = this._ordinals[index]; - - // Record old height - let heightInPx = this._heights[index]; - - // Record old min width - let minWidth = this._minWidths[index]; - - // Since changing `afterLineNumber` can trigger a reordering, we're gonna remove this whitespace - this.removeWhitespace(id); - - // And add it again - let insertionIndex = WhitespaceComputer.findInsertionIndex(this._afterLineNumbers, newAfterLineNumber, this._ordinals, ordinal); - this._insertWhitespaceAtIndex(id, insertionIndex, newAfterLineNumber, ordinal, heightInPx, minWidth); - - return true; - } - } - return false; - } - - /** - * Remove an existing whitespace. - * - * @param id The whitespace to remove - * @return Returns true if the whitespace is found and it is removed. - */ - public removeWhitespace(id: string): boolean { - if (this._whitespaceId2Index.hasOwnProperty(id)) { - let index = this._whitespaceId2Index[id]; - delete this._whitespaceId2Index[id]; - this._removeWhitespaceAtIndex(index); - this._minWidth = -1; /* marker for not being computed */ - return true; - } - - return false; - } - - private _removeWhitespaceAtIndex(removeIndex: number): void { - removeIndex = removeIndex | 0; - - this._heights.splice(removeIndex, 1); - this._minWidths.splice(removeIndex, 1); - this._ids.splice(removeIndex, 1); - this._afterLineNumbers.splice(removeIndex, 1); - this._ordinals.splice(removeIndex, 1); - this._prefixSum.splice(removeIndex, 1); - this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, removeIndex - 1); - - let keys = Object.keys(this._whitespaceId2Index); - for (let i = 0, len = keys.length; i < len; i++) { - let sid = keys[i]; - let oldIndex = this._whitespaceId2Index[sid]; - if (oldIndex >= removeIndex) { - this._whitespaceId2Index[sid] = oldIndex - 1; - } - } - } - - /** - * Notify the computer that lines have been deleted (a continuous zone of lines). - * This gives it a chance to update `afterLineNumber` for whitespaces, giving the "sticky" characteristic. - * - * @param fromLineNumber The line number at which the deletion started, inclusive - * @param toLineNumber The line number at which the deletion ended, inclusive - */ - public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { - fromLineNumber = fromLineNumber | 0; - toLineNumber = toLineNumber | 0; - - for (let i = 0, len = this._afterLineNumbers.length; i < len; i++) { - let afterLineNumber = this._afterLineNumbers[i]; - - if (fromLineNumber <= afterLineNumber && afterLineNumber <= toLineNumber) { - // The line this whitespace was after has been deleted - // => move whitespace to before first deleted line - this._afterLineNumbers[i] = fromLineNumber - 1; - } else if (afterLineNumber > toLineNumber) { - // The line this whitespace was after has been moved up - // => move whitespace up - this._afterLineNumbers[i] -= (toLineNumber - fromLineNumber + 1); - } - } - } - - /** - * Notify the computer that lines have been inserted (a continuous zone of lines). - * This gives it a chance to update `afterLineNumber` for whitespaces, giving the "sticky" characteristic. - * - * @param fromLineNumber The line number at which the insertion started, inclusive - * @param toLineNumber The line number at which the insertion ended, inclusive. - */ - public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { - fromLineNumber = fromLineNumber | 0; - toLineNumber = toLineNumber | 0; - - for (let i = 0, len = this._afterLineNumbers.length; i < len; i++) { - let afterLineNumber = this._afterLineNumbers[i]; - - if (fromLineNumber <= afterLineNumber) { - this._afterLineNumbers[i] += (toLineNumber - fromLineNumber + 1); - } - } - } - - /** - * Get the sum of all the whitespaces. - */ - public getTotalHeight(): number { - if (this._heights.length === 0) { - return 0; - } - return this.getAccumulatedHeight(this._heights.length - 1); - } - - /** - * Return the sum of the heights of the whitespaces at [0..index]. - * This includes the whitespace at `index`. - * - * @param index The index of the whitespace. - * @return The sum of the heights of all whitespaces before the one at `index`, including the one at `index`. - */ - public getAccumulatedHeight(index: number): number { - index = index | 0; - - let startIndex = Math.max(0, this._prefixSumValidIndex + 1); - if (startIndex === 0) { - this._prefixSum[0] = this._heights[0]; - startIndex++; - } - - for (let i = startIndex; i <= index; i++) { - this._prefixSum[i] = this._prefixSum[i - 1] + this._heights[i]; - } - this._prefixSumValidIndex = Math.max(this._prefixSumValidIndex, index); - return this._prefixSum[index]; - } - - /** - * Find all whitespaces with `afterLineNumber` < `lineNumber` and return the sum of their heights. - * - * @param lineNumber The line number whitespaces should be before. - * @return The sum of the heights of the whitespaces before `lineNumber`. - */ - public getAccumulatedHeightBeforeLineNumber(lineNumber: number): number { - lineNumber = lineNumber | 0; - - let lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeLineNumber(lineNumber); - - if (lastWhitespaceBeforeLineNumber === -1) { - return 0; - } - - return this.getAccumulatedHeight(lastWhitespaceBeforeLineNumber); - } - - private _findLastWhitespaceBeforeLineNumber(lineNumber: number): number { - lineNumber = lineNumber | 0; - - // Find the whitespace before line number - let afterLineNumbers = this._afterLineNumbers; - let low = 0; - let high = afterLineNumbers.length - 1; - - while (low <= high) { - let delta = (high - low) | 0; - let halfDelta = (delta / 2) | 0; - let mid = (low + halfDelta) | 0; - - if (afterLineNumbers[mid] < lineNumber) { - if (mid + 1 >= afterLineNumbers.length || afterLineNumbers[mid + 1] >= lineNumber) { - return mid; - } else { - low = (mid + 1) | 0; - } - } else { - high = (mid - 1) | 0; - } - } - - return -1; - } - - private _findFirstWhitespaceAfterLineNumber(lineNumber: number): number { - lineNumber = lineNumber | 0; - - let lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeLineNumber(lineNumber); - let firstWhitespaceAfterLineNumber = lastWhitespaceBeforeLineNumber + 1; - - if (firstWhitespaceAfterLineNumber < this._heights.length) { - return firstWhitespaceAfterLineNumber; - } - - return -1; - } - - /** - * Find the index of the first whitespace which has `afterLineNumber` >= `lineNumber`. - * @return The index of the first whitespace with `afterLineNumber` >= `lineNumber` or -1 if no whitespace is found. - */ - public getFirstWhitespaceIndexAfterLineNumber(lineNumber: number): number { - lineNumber = lineNumber | 0; - - return this._findFirstWhitespaceAfterLineNumber(lineNumber); - } - - /** - * The number of whitespaces. - */ - public getCount(): number { - return this._heights.length; - } - - /** - * The maximum min width for all whitespaces. - */ - public getMinWidth(): number { - if (this._minWidth === -1) { - let minWidth = 0; - for (let i = 0, len = this._minWidths.length; i < len; i++) { - minWidth = Math.max(minWidth, this._minWidths[i]); - } - this._minWidth = minWidth; - } - return this._minWidth; - } - - /** - * Get the `afterLineNumber` for whitespace at index `index`. - * - * @param index The index of the whitespace. - * @return `afterLineNumber` of whitespace at `index`. - */ - public getAfterLineNumberForWhitespaceIndex(index: number): number { - index = index | 0; - - return this._afterLineNumbers[index]; - } - - /** - * Get the `id` for whitespace at index `index`. - * - * @param index The index of the whitespace. - * @return `id` of whitespace at `index`. - */ - public getIdForWhitespaceIndex(index: number): string { - index = index | 0; - - return this._ids[index]; - } - - /** - * Get the `height` for whitespace at index `index`. - * - * @param index The index of the whitespace. - * @return `height` of whitespace at `index`. - */ - public getHeightForWhitespaceIndex(index: number): number { - index = index | 0; - - return this._heights[index]; - } - - /** - * Get all whitespaces. - */ - public getWhitespaces(deviceLineHeight: number): IEditorWhitespace[] { - deviceLineHeight = deviceLineHeight | 0; - - let result: IEditorWhitespace[] = []; - for (let i = 0; i < this._heights.length; i++) { - result.push({ - id: this._ids[i], - afterLineNumber: this._afterLineNumbers[i], - heightInLines: this._heights[i] / deviceLineHeight - }); - } - return result; - } -} diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index 0f16d0a84a2..a3abd74ee4e 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -13,7 +13,7 @@ import { INewScrollPosition } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions } from 'vs/editor/common/model'; import { IViewEventListener } from 'vs/editor/common/view/viewEvents'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; +import { IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; import { ITheme } from 'vs/platform/theme/common/themeService'; export interface IViewWhitespaceViewportData { @@ -69,20 +69,8 @@ export interface IViewLayout { getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null; // --------------- Begin vertical whitespace management + changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => T): T; - /** - * Reserve rendering space. - * @return an identifier that can be later used to remove or change the whitespace. - */ - addWhitespace(afterLineNumber: number, ordinal: number, height: number, minWidth: number): string; - /** - * Change the properties of a whitespace. - */ - changeWhitespace(id: string, newAfterLineNumber: number, newHeight: number): boolean; - /** - * Remove rendering space - */ - removeWhitespace(id: string): boolean; /** * Get the layout information for whitespaces currently in the viewport */ diff --git a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts index 0c3f47322b8..98ce2b09ca5 100644 --- a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts @@ -14,7 +14,6 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel'; import { editorHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { BrowserFeatures } from 'vs/base/browser/canIUse'; const $ = dom.$; @@ -151,7 +150,7 @@ class SaturationBox extends Disposable { this.layout(); - this._register(dom.addDisposableListener(this.domNode, BrowserFeatures.pointerEvents ? dom.EventType.POINTER_DOWN : dom.EventType.MOUSE_DOWN, e => this.onMouseDown(e))); + this._register(dom.addDisposableGenericMouseDownListner(this.domNode, e => this.onMouseDown(e))); this._register(this.model.onDidChangeColor(this.onDidChangeColor, this)); this.monitor = null; } @@ -166,7 +165,7 @@ class SaturationBox extends Disposable { this.monitor.startMonitoring(standardMouseMoveMerger, event => this.onDidChangePosition(event.posx - origin.left, event.posy - origin.top), () => null); - const mouseUpListener = dom.addDisposableListener(document, BrowserFeatures.pointerEvents ? dom.EventType.POINTER_UP : dom.EventType.MOUSE_UP, () => { + const mouseUpListener = dom.addDisposableGenericMouseUpListner(document, () => { this._onColorFlushed.fire(); mouseUpListener.dispose(); if (this.monitor) { @@ -251,7 +250,7 @@ abstract class Strip extends Disposable { this.slider = dom.append(this.domNode, $('.slider')); this.slider.style.top = `0px`; - this._register(dom.addDisposableListener(this.domNode, BrowserFeatures.pointerEvents ? dom.EventType.POINTER_DOWN : dom.EventType.MOUSE_DOWN, e => this.onMouseDown(e))); + this._register(dom.addDisposableGenericMouseDownListner(this.domNode, e => this.onMouseDown(e))); this.layout(); } @@ -273,7 +272,7 @@ abstract class Strip extends Disposable { monitor.startMonitoring(standardMouseMoveMerger, event => this.onDidChangeTop(event.posy - origin.top), () => null); - const mouseUpListener = dom.addDisposableListener(document, BrowserFeatures.pointerEvents ? dom.EventType.POINTER_UP : dom.EventType.MOUSE_UP, () => { + const mouseUpListener = dom.addDisposableGenericMouseUpListner(document, () => { this._onColorFlushed.fire(); mouseUpListener.dispose(); monitor.stopMonitoring(true); diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index f9c0cd00902..6d4226cbe5e 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -8,7 +8,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { ICodeEditor, IEditorMouseEvent, IMouseTarget, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IEditorMouseEvent, IMouseTarget, MouseTargetType, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { Position } from 'vs/editor/common/core/position'; @@ -50,7 +50,7 @@ export class DragAndDropController extends Disposable implements editorCommon.IE this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(e))); this._register(this._editor.onMouseDrag((e: IEditorMouseEvent) => this._onEditorMouseDrag(e))); - this._register(this._editor.onMouseDrop((e: IEditorMouseEvent) => this._onEditorMouseDrop(e))); + this._register(this._editor.onMouseDrop((e: IPartialEditorMouseEvent) => this._onEditorMouseDrop(e))); this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(e))); this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(e))); this._register(this._editor.onDidBlurEditorWidget(() => this.onEditorBlur())); @@ -143,7 +143,7 @@ export class DragAndDropController extends Disposable implements editorCommon.IE } } - private _onEditorMouseDrop(mouseEvent: IEditorMouseEvent): void { + private _onEditorMouseDrop(mouseEvent: IPartialEditorMouseEvent): void { if (mouseEvent.target && (this._hitContent(mouseEvent.target) || this._hitMargin(mouseEvent.target)) && mouseEvent.target.position) { let newCursorPosition = new Position(mouseEvent.target.position.lineNumber, mouseEvent.target.position.column); diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index 5cabcc69c61..a06f769de38 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -65,7 +65,6 @@ .monaco-editor .find-widget .monaco-inputbox .input { background-color: transparent; - /* Style to compensate for //winjs */ min-height: 0; } diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index d000560f648..86f04e0db86 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -423,7 +423,7 @@ export class FoldingController extends Disposable implements IEditorContribution if (iconClicked || isCollapsed) { let toToggle = [region]; if (e.event.middleButton || e.event.shiftKey) { - toToggle.push(...foldingModel.getRegionsInside(region, r => r.isCollapsed === isCollapsed)); + toToggle.push(...foldingModel.getRegionsInside(region, (r: FoldingRegion) => r.isCollapsed === isCollapsed)); } foldingModel.toggleCollapseState(toToggle); this.reveal({ lineNumber, column: 1 }); diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts index 44feb79cdf2..93a52a14728 100644 --- a/src/vs/editor/contrib/folding/foldingModel.ts +++ b/src/vs/editor/contrib/folding/foldingModel.ts @@ -204,7 +204,7 @@ export class FoldingModel { return null; } - getRegionsInside(region: FoldingRegion | null, filter?: (r: FoldingRegion, level?: number) => boolean): FoldingRegion[] { + getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] { let result: FoldingRegion[] = []; let index = region ? region.regionIndex + 1 : 0; let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE; @@ -229,7 +229,7 @@ export class FoldingModel { for (let i = index, len = this._regions.length; i < len; i++) { let current = this._regions.toRegion(i); if (this._regions.getStartLineNumber(i) < endLineNumber) { - if (!filter || filter(current)) { + if (!filter || (filter as RegionFilter)(current)) { result.push(current); } } else { @@ -242,6 +242,10 @@ export class FoldingModel { } +type RegionFilter = (r: FoldingRegion) => boolean; +type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean; + + /** * Collapse or expand the regions at the given locations * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels. diff --git a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts index 5ec0555a5ef..b9fbc7182ce 100644 --- a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts @@ -119,8 +119,9 @@ abstract class SymbolNavigationAction extends EditorAction { } else { const next = model.firstReference()!; - const targetEditor = await this._openReference(editor, editorService, next, this._configuration.openToSide); - if (targetEditor && model.references.length > 1 && gotoLocation === 'gotoAndPeek') { + const peek = model.references.length > 1 && gotoLocation === 'gotoAndPeek'; + const targetEditor = await this._openReference(editor, editorService, next, this._configuration.openToSide, !peek); + if (peek && targetEditor) { this._openInPeek(targetEditor, model); } else { model.dispose(); @@ -134,7 +135,7 @@ abstract class SymbolNavigationAction extends EditorAction { } } - private _openReference(editor: ICodeEditor, editorService: ICodeEditorService, reference: Location | LocationLink, sideBySide: boolean): Promise { + private async _openReference(editor: ICodeEditor, editorService: ICodeEditorService, reference: Location | LocationLink, sideBySide: boolean, highlight: boolean): Promise { // range is the target-selection-range when we have one // and the the fallback is the 'full' range let range: IRange | undefined = undefined; @@ -145,13 +146,29 @@ abstract class SymbolNavigationAction extends EditorAction { range = reference.range; } - return editorService.openCodeEditor({ + const targetEditor = await editorService.openCodeEditor({ resource: reference.uri, options: { selection: Range.collapseToStart(range), revealInCenterIfOutsideViewport: true } }, editor, sideBySide); + + if (!targetEditor) { + return undefined; + } + + if (highlight) { + const modelNow = targetEditor.getModel(); + const ids = targetEditor.deltaDecorations([], [{ range, options: { className: 'rangeHighlight' } }]); + setTimeout(() => { + if (targetEditor.getModel() === modelNow) { + targetEditor.deltaDecorations(ids, []); + } + }, 350); + } + + return targetEditor; } private _openInPeek(target: ICodeEditor, model: ReferencesModel) { diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index f37254d776f..ca710263b24 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -207,7 +207,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { private readonly _themeService: IThemeService, private readonly _keybindingService: IKeybindingService, private readonly _modeService: IModeService, - private readonly _openerService: IOpenerService | null = NullOpenerService, + private readonly _openerService: IOpenerService = NullOpenerService, ) { super(ModesContentHoverWidget.ID, editor); diff --git a/src/vs/editor/contrib/hover/modesGlyphHover.ts b/src/vs/editor/contrib/hover/modesGlyphHover.ts index 85dece77b64..32c6cf14656 100644 --- a/src/vs/editor/contrib/hover/modesGlyphHover.ts +++ b/src/vs/editor/contrib/hover/modesGlyphHover.ts @@ -97,7 +97,7 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { constructor( editor: ICodeEditor, modeService: IModeService, - openerService: IOpenerService | null = NullOpenerService, + openerService: IOpenerService = NullOpenerService, ) { super(ModesGlyphHoverWidget.ID, editor); diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index df34eed5600..a85e7f8c942 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -44,17 +44,9 @@ export class Link implements ILink { return this._link.tooltip; } - resolve(token: CancellationToken): Promise { + async resolve(token: CancellationToken): Promise { if (this._link.url) { - try { - if (typeof this._link.url === 'string') { - return Promise.resolve(URI.parse(this._link.url)); - } else { - return Promise.resolve(this._link.url); - } - } catch (e) { - return Promise.reject(new Error('invalid')); - } + return this._link.url; } if (typeof this._provider.resolveLink === 'function') { diff --git a/src/vs/editor/contrib/markdown/markdownRenderer.ts b/src/vs/editor/contrib/markdown/markdownRenderer.ts index eb17228cfa8..b4bfe8ddd9e 100644 --- a/src/vs/editor/contrib/markdown/markdownRenderer.ts +++ b/src/vs/editor/contrib/markdown/markdownRenderer.ts @@ -7,7 +7,6 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { renderMarkdown, MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -29,7 +28,7 @@ export class MarkdownRenderer extends Disposable { constructor( private readonly _editor: ICodeEditor, @IModeService private readonly _modeService: IModeService, - @optional(IOpenerService) private readonly _openerService: IOpenerService | null = NullOpenerService, + @optional(IOpenerService) private readonly _openerService: IOpenerService = NullOpenerService, ) { super(); } @@ -64,15 +63,7 @@ export class MarkdownRenderer extends Disposable { codeBlockRenderCallback: () => this._onDidRenderCodeBlock.fire(), actionHandler: { callback: (content) => { - let uri: URI | undefined; - try { - uri = URI.parse(content); - } catch { - // ignore - } - if (uri && this._openerService) { - this._openerService.open(uri, { fromUserGesture: true }).catch(onUnexpectedError); - } + this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError); }, disposeables } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 1777683f5cc..b9abf518d33 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -89,7 +89,7 @@ export module StaticServices { let _all: LazyStaticService[] = []; - function define(serviceId: ServiceIdentifier, factory: (overrides: IEditorOverrideServices) => T): LazyStaticService { + function define(serviceId: ServiceIdentifier, factory: (overrides: IEditorOverrideServices | undefined) => T): LazyStaticService { let r = new LazyStaticService(serviceId, factory); _all.push(r); return r; diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 6f03b911e37..697dc99402d 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands'; +import { matchesScheme } from 'vs/platform/opener/common/opener'; suite('OpenerService', function () { const editorService = new TestCodeEditorService(); @@ -28,27 +29,27 @@ suite('OpenerService', function () { lastCommand = undefined; }); - test('delegate to editorService, scheme:///fff', function () { + test('delegate to editorService, scheme:///fff', async function () { const openerService = new OpenerService(editorService, NullCommandService); - openerService.open(URI.parse('another:///somepath')); + await openerService.open(URI.parse('another:///somepath')); assert.equal(editorService.lastInput!.options!.selection, undefined); }); - test('delegate to editorService, scheme:///fff#L123', function () { + test('delegate to editorService, scheme:///fff#L123', async function () { const openerService = new OpenerService(editorService, NullCommandService); - openerService.open(URI.parse('file:///somepath#L23')); + await openerService.open(URI.parse('file:///somepath#L23')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1); assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined); assert.equal(editorService.lastInput!.options!.selection!.endColumn, undefined); assert.equal(editorService.lastInput!.resource.fragment, ''); - openerService.open(URI.parse('another:///somepath#L23')); + await openerService.open(URI.parse('another:///somepath#L23')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1); - openerService.open(URI.parse('another:///somepath#L23,45')); + await openerService.open(URI.parse('another:///somepath#L23,45')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 45); assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined); @@ -56,17 +57,17 @@ suite('OpenerService', function () { assert.equal(editorService.lastInput!.resource.fragment, ''); }); - test('delegate to editorService, scheme:///fff#123,123', function () { + test('delegate to editorService, scheme:///fff#123,123', async function () { const openerService = new OpenerService(editorService, NullCommandService); - openerService.open(URI.parse('file:///somepath#23')); + await openerService.open(URI.parse('file:///somepath#23')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1); assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined); assert.equal(editorService.lastInput!.options!.selection!.endColumn, undefined); assert.equal(editorService.lastInput!.resource.fragment, ''); - openerService.open(URI.parse('file:///somepath#23,45')); + await openerService.open(URI.parse('file:///somepath#23,45')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 45); assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined); @@ -74,22 +75,22 @@ suite('OpenerService', function () { assert.equal(editorService.lastInput!.resource.fragment, ''); }); - test('delegate to commandsService, command:someid', function () { + test('delegate to commandsService, command:someid', async function () { const openerService = new OpenerService(editorService, commandService); const id = `aCommand${Math.random()}`; CommandsRegistry.registerCommand(id, function () { }); - openerService.open(URI.parse('command:' + id)); + await openerService.open(URI.parse('command:' + id)); assert.equal(lastCommand!.id, id); assert.equal(lastCommand!.args.length, 0); - openerService.open(URI.parse('command:' + id).with({ query: '123' })); + await openerService.open(URI.parse('command:' + id).with({ query: '123' })); assert.equal(lastCommand!.id, id); assert.equal(lastCommand!.args.length, 1); assert.equal(lastCommand!.args[0], '123'); - openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) })); + await openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) })); assert.equal(lastCommand!.id, id); assert.equal(lastCommand!.args.length, 2); assert.equal(lastCommand!.args[0], 12); @@ -199,4 +200,18 @@ suite('OpenerService', function () { assert.equal(v1, 2); assert.equal(v2, 0); }); + + test('matchesScheme', function () { + assert.ok(matchesScheme('https://microsoft.com', 'https')); + assert.ok(matchesScheme('http://microsoft.com', 'http')); + assert.ok(matchesScheme('hTTPs://microsoft.com', 'https')); + assert.ok(matchesScheme('httP://microsoft.com', 'http')); + assert.ok(matchesScheme(URI.parse('https://microsoft.com'), 'https')); + assert.ok(matchesScheme(URI.parse('http://microsoft.com'), 'http')); + assert.ok(matchesScheme(URI.parse('hTTPs://microsoft.com'), 'https')); + assert.ok(matchesScheme(URI.parse('httP://microsoft.com'), 'http')); + assert.ok(!matchesScheme(URI.parse('https://microsoft.com'), 'http')); + assert.ok(!matchesScheme(URI.parse('htt://microsoft.com'), 'http')); + assert.ok(!matchesScheme(URI.parse('z://microsoft.com'), 'http')); + }); }); diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index dceeaab37b0..b6b26b7f35a 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -3,10 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { LinesLayout } from 'vs/editor/common/viewLayout/linesLayout'; +import { LinesLayout, EditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; suite('Editor ViewLayout - LinesLayout', () => { + function insertWhitespace(linesLayout: LinesLayout, afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string { + return linesLayout.changeWhitespace((accessor) => { + return accessor.insertWhitespace(afterLineNumber, ordinal, heightInPx, minWidth); + }); + } + + function changeOneWhitespace(linesLayout: LinesLayout, id: string, newAfterLineNumber: number, newHeight: number): void { + linesLayout.changeWhitespace((accessor) => { + accessor.changeOneWhitespace(id, newAfterLineNumber, newHeight); + }); + } + + function removeWhitespace(linesLayout: LinesLayout, id: string): void { + linesLayout.changeWhitespace((accessor) => { + accessor.removeWhitespace(id); + }); + } + test('LinesLayout 1', () => { // Start off with 10 lines @@ -39,7 +57,7 @@ suite('Editor ViewLayout - LinesLayout', () => { assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(29), 3); // Add whitespace of height 5px after 2nd line - linesLayout.insertWhitespace(2, 0, 5, 0); + insertWhitespace(linesLayout, 2, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5) assert.equal(linesLayout.getLinesTotalHeight(), 105); @@ -63,8 +81,8 @@ suite('Editor ViewLayout - LinesLayout', () => { assert.equal(linesLayout.getLineNumberAtOrAfterVerticalOffset(105), 10); // Add two more whitespaces of height 5px - linesLayout.insertWhitespace(3, 0, 5, 0); - linesLayout.insertWhitespace(4, 0, 5, 0); + insertWhitespace(linesLayout, 3, 0, 5, 0); + insertWhitespace(linesLayout, 4, 0, 5, 0); // lines: [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] // whitespace: a(2,5), b(3, 5), c(4, 5) assert.equal(linesLayout.getLinesTotalHeight(), 115); @@ -120,7 +138,7 @@ suite('Editor ViewLayout - LinesLayout', () => { // Start off with 10 lines and one whitespace after line 2, of height 5 let linesLayout = new LinesLayout(10, 1); - let a = linesLayout.insertWhitespace(2, 0, 5, 0); + let a = insertWhitespace(linesLayout, 2, 0, 5, 0); // 10 lines // whitespace: - a(2,5) @@ -139,7 +157,7 @@ suite('Editor ViewLayout - LinesLayout', () => { // Change whitespace height // 10 lines // whitespace: - a(2,10) - linesLayout.changeWhitespace(a, 2, 10); + changeOneWhitespace(linesLayout, a, 2, 10); assert.equal(linesLayout.getLinesTotalHeight(), 20); assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); @@ -155,7 +173,7 @@ suite('Editor ViewLayout - LinesLayout', () => { // Change whitespace position // 10 lines // whitespace: - a(5,10) - linesLayout.changeWhitespace(a, 5, 10); + changeOneWhitespace(linesLayout, a, 5, 10); assert.equal(linesLayout.getLinesTotalHeight(), 20); assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); @@ -200,7 +218,7 @@ suite('Editor ViewLayout - LinesLayout', () => { // Remove whitespace // 10 lines - linesLayout.removeWhitespace(a); + removeWhitespace(linesLayout, a); assert.equal(linesLayout.getLinesTotalHeight(), 10); assert.equal(linesLayout.getVerticalOffsetForLineNumber(1), 0); assert.equal(linesLayout.getVerticalOffsetForLineNumber(2), 1); @@ -216,7 +234,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout getLineNumberAtOrAfterVerticalOffset', () => { let linesLayout = new LinesLayout(10, 1); - linesLayout.insertWhitespace(6, 0, 10, 0); + insertWhitespace(linesLayout, 6, 0, 10, 0); // 10 lines // whitespace: - a(6,10) @@ -265,7 +283,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout getCenteredLineInViewport', () => { let linesLayout = new LinesLayout(10, 1); - linesLayout.insertWhitespace(6, 0, 10, 0); + insertWhitespace(linesLayout, 6, 0, 10, 0); // 10 lines // whitespace: - a(6,10) @@ -348,7 +366,7 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout getLinesViewportData 1', () => { let linesLayout = new LinesLayout(10, 10); - linesLayout.insertWhitespace(6, 0, 100, 0); + insertWhitespace(linesLayout, 6, 0, 100, 0); // 10 lines // whitespace: - a(6,100) @@ -479,11 +497,10 @@ suite('Editor ViewLayout - LinesLayout', () => { assert.deepEqual(viewportData.relativeVerticalOffset, [160, 170, 180, 190]); }); - test('LinesLayout getLinesViewportData 2 & getWhitespaceViewportData', () => { let linesLayout = new LinesLayout(10, 10); - let a = linesLayout.insertWhitespace(6, 0, 100, 0); - let b = linesLayout.insertWhitespace(7, 0, 50, 0); + let a = insertWhitespace(linesLayout, 6, 0, 100, 0); + let b = insertWhitespace(linesLayout, 7, 0, 50, 0); // 10 lines // whitespace: - a(6,100), b(7, 50) @@ -553,8 +570,8 @@ suite('Editor ViewLayout - LinesLayout', () => { test('LinesLayout getWhitespaceAtVerticalOffset', () => { let linesLayout = new LinesLayout(10, 10); - let a = linesLayout.insertWhitespace(6, 0, 100, 0); - let b = linesLayout.insertWhitespace(7, 0, 50, 0); + let a = insertWhitespace(linesLayout, 6, 0, 100, 0); + let b = insertWhitespace(linesLayout, 7, 0, 50, 0); let whitespace = linesLayout.getWhitespaceAtVerticalOffset(0); assert.equal(whitespace, null); @@ -592,4 +609,536 @@ suite('Editor ViewLayout - LinesLayout', () => { whitespace = linesLayout.getWhitespaceAtVerticalOffset(220); assert.equal(whitespace, null); }); + + test('LinesLayout', () => { + + const linesLayout = new LinesLayout(100, 20); + + // Insert a whitespace after line number 2, of height 10 + const a = insertWhitespace(linesLayout, 2, 0, 10, 0); + // whitespaces: a(2, 10) + assert.equal(linesLayout.getWhitespacesCount(), 1); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 10); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); + + // Insert a whitespace again after line number 2, of height 20 + let b = insertWhitespace(linesLayout, 2, 0, 20, 0); + // whitespaces: a(2, 10), b(2, 20) + assert.equal(linesLayout.getWhitespacesCount(), 2); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 30); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 30); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + + // Change last inserted whitespace height to 30 + changeOneWhitespace(linesLayout, b, 2, 30); + // whitespaces: a(2, 10), b(2, 30) + assert.equal(linesLayout.getWhitespacesCount(), 2); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 30); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 40); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 40); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 40); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 40); + + // Remove last inserted whitespace + removeWhitespace(linesLayout, b); + // whitespaces: a(2, 10) + assert.equal(linesLayout.getWhitespacesCount(), 1); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 10); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 10); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 10); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 10); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 10); + + // Add a whitespace before the first line of height 50 + b = insertWhitespace(linesLayout, 0, 0, 50, 0); + // whitespaces: b(0, 50), a(2, 10) + assert.equal(linesLayout.getWhitespacesCount(), 2); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 60); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); + + // Add a whitespace after line 4 of height 20 + insertWhitespace(linesLayout, 4, 0, 20, 0); + // whitespaces: b(0, 50), a(2, 10), c(4, 20) + assert.equal(linesLayout.getWhitespacesCount(), 3); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); + assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 80); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 80); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 60); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 80); + + // Add a whitespace after line 3 of height 30 + insertWhitespace(linesLayout, 3, 0, 30, 0); + // whitespaces: b(0, 50), a(2, 10), d(3, 30), c(4, 20) + assert.equal(linesLayout.getWhitespacesCount(), 4); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 10); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); + assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 30); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); + assert.equal(linesLayout.getHeightForWhitespaceIndex(3), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 60); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 90); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(3), 110); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 110); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 60); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 90); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 110); + + // Change whitespace after line 2 to height of 100 + changeOneWhitespace(linesLayout, a, 2, 100); + // whitespaces: b(0, 50), a(2, 100), d(3, 30), c(4, 20) + assert.equal(linesLayout.getWhitespacesCount(), 4); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 100); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 3); + assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 30); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(3), 4); + assert.equal(linesLayout.getHeightForWhitespaceIndex(3), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 150); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 180); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(3), 200); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 200); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 150); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 180); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 200); + + // Remove whitespace after line 2 + removeWhitespace(linesLayout, a); + // whitespaces: b(0, 50), d(3, 30), c(4, 20) + assert.equal(linesLayout.getWhitespacesCount(), 3); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 50); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 30); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 4); + assert.equal(linesLayout.getHeightForWhitespaceIndex(2), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 50); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 80); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(2), 100); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 100); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 80); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 100); + + // Remove whitespace before line 1 + removeWhitespace(linesLayout, b); + // whitespaces: d(3, 30), c(4, 20) + assert.equal(linesLayout.getWhitespacesCount(), 2); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + + // Delete line 1 + linesLayout.onLinesDeleted(1, 1); + // whitespaces: d(2, 30), c(3, 20) + assert.equal(linesLayout.getWhitespacesCount(), 2); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 30); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + + // Insert a line before line 1 + linesLayout.onLinesInserted(1, 1); + // whitespaces: d(3, 30), c(4, 20) + assert.equal(linesLayout.getWhitespacesCount(), 2); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 4); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 30); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + + // Delete line 4 + linesLayout.onLinesDeleted(4, 4); + // whitespaces: d(3, 30), c(3, 20) + assert.equal(linesLayout.getWhitespacesCount(), 2); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.equal(linesLayout.getHeightForWhitespaceIndex(0), 30); + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.equal(linesLayout.getHeightForWhitespaceIndex(1), 20); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(0), 30); + assert.equal(linesLayout.getWhitespacesAccumulatedHeight(1), 50); + assert.equal(linesLayout.getWhitespacesTotalHeight(), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(1), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(2), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(3), 0); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(4), 50); + assert.equal(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); + }); + + test('LinesLayout findInsertionIndex', () => { + + const makeInternalWhitespace = (afterLineNumbers: number[], ordinal: number = 0) => { + return afterLineNumbers.map((afterLineNumber) => new EditorWhitespace('', afterLineNumber, ordinal, 0, 0)); + }; + + let arr: EditorWhitespace[]; + + arr = makeInternalWhitespace([]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 0); + + arr = makeInternalWhitespace([1]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + + arr = makeInternalWhitespace([1, 3]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + + arr = makeInternalWhitespace([1, 3, 5]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + + arr = makeInternalWhitespace([1, 3, 5], 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + + arr = makeInternalWhitespace([1, 3, 5, 7]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + + arr = makeInternalWhitespace([1, 3, 5, 7, 9]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + + arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + + arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11, 13]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.equal(LinesLayout.findInsertionIndex(arr, 13, 0), 7); + assert.equal(LinesLayout.findInsertionIndex(arr, 14, 0), 7); + + arr = makeInternalWhitespace([1, 3, 5, 7, 9, 11, 13, 15]); + assert.equal(LinesLayout.findInsertionIndex(arr, 0, 0), 0); + assert.equal(LinesLayout.findInsertionIndex(arr, 1, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 2, 0), 1); + assert.equal(LinesLayout.findInsertionIndex(arr, 3, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 4, 0), 2); + assert.equal(LinesLayout.findInsertionIndex(arr, 5, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 6, 0), 3); + assert.equal(LinesLayout.findInsertionIndex(arr, 7, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 8, 0), 4); + assert.equal(LinesLayout.findInsertionIndex(arr, 9, 0), 5); + assert.equal(LinesLayout.findInsertionIndex(arr, 10, 0), 5); + assert.equal(LinesLayout.findInsertionIndex(arr, 11, 0), 6); + assert.equal(LinesLayout.findInsertionIndex(arr, 12, 0), 6); + assert.equal(LinesLayout.findInsertionIndex(arr, 13, 0), 7); + assert.equal(LinesLayout.findInsertionIndex(arr, 14, 0), 7); + assert.equal(LinesLayout.findInsertionIndex(arr, 15, 0), 8); + assert.equal(LinesLayout.findInsertionIndex(arr, 16, 0), 8); + }); + + test('LinesLayout changeWhitespaceAfterLineNumber & getFirstWhitespaceIndexAfterLineNumber', () => { + const linesLayout = new LinesLayout(100, 20); + + const a = insertWhitespace(linesLayout, 0, 0, 1, 0); + const b = insertWhitespace(linesLayout, 7, 0, 1, 0); + const c = insertWhitespace(linesLayout, 3, 0, 1, 0); + + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 0); + assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 1); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + + // Do not really move a + changeOneWhitespace(linesLayout, a, 1, 1); + + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 1 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 1); + assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + + + // Do not really move a + changeOneWhitespace(linesLayout, a, 2, 1); + + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 2 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 2); + assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + + + // Change a to conflict with c => a gets placed after c + changeOneWhitespace(linesLayout, a, 3, 1); + + assert.equal(linesLayout.getIdForWhitespaceIndex(0), c); // 3 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.equal(linesLayout.getIdForWhitespaceIndex(1), a); // 3 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + + + // Make a no-op + changeOneWhitespace(linesLayout, c, 3, 1); + + assert.equal(linesLayout.getIdForWhitespaceIndex(0), c); // 3 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.equal(linesLayout.getIdForWhitespaceIndex(1), a); // 3 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 3); + assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + + + + // Conflict c with b => c gets placed after b + changeOneWhitespace(linesLayout, c, 7, 1); + + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 3 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); + assert.equal(linesLayout.getIdForWhitespaceIndex(1), b); // 7 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(1), 7); + assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 7 + assert.equal(linesLayout.getAfterLineNumberForWhitespaceIndex(2), 7); + + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(3), 0); // a + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(4), 1); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(5), 1); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(6), 1); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(7), 1); // b + assert.equal(linesLayout.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- + }); + + test('LinesLayout Bug', () => { + const linesLayout = new LinesLayout(100, 20); + + const a = insertWhitespace(linesLayout, 0, 0, 1, 0); + const b = insertWhitespace(linesLayout, 7, 0, 1, 0); + + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.equal(linesLayout.getIdForWhitespaceIndex(1), b); // 7 + + const c = insertWhitespace(linesLayout, 3, 0, 1, 0); + + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.equal(linesLayout.getIdForWhitespaceIndex(1), c); // 3 + assert.equal(linesLayout.getIdForWhitespaceIndex(2), b); // 7 + + const d = insertWhitespace(linesLayout, 2, 0, 1, 0); + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + + const e = insertWhitespace(linesLayout, 8, 0, 1, 0); + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + + const f = insertWhitespace(linesLayout, 11, 0, 1, 0); + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.equal(linesLayout.getIdForWhitespaceIndex(5), f); // 11 + + const g = insertWhitespace(linesLayout, 10, 0, 1, 0); + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.equal(linesLayout.getIdForWhitespaceIndex(1), d); // 2 + assert.equal(linesLayout.getIdForWhitespaceIndex(2), c); // 3 + assert.equal(linesLayout.getIdForWhitespaceIndex(3), b); // 7 + assert.equal(linesLayout.getIdForWhitespaceIndex(4), e); // 8 + assert.equal(linesLayout.getIdForWhitespaceIndex(5), g); // 10 + assert.equal(linesLayout.getIdForWhitespaceIndex(6), f); // 11 + + const h = insertWhitespace(linesLayout, 0, 0, 1, 0); + assert.equal(linesLayout.getIdForWhitespaceIndex(0), a); // 0 + assert.equal(linesLayout.getIdForWhitespaceIndex(1), h); // 0 + assert.equal(linesLayout.getIdForWhitespaceIndex(2), d); // 2 + assert.equal(linesLayout.getIdForWhitespaceIndex(3), c); // 3 + assert.equal(linesLayout.getIdForWhitespaceIndex(4), b); // 7 + assert.equal(linesLayout.getIdForWhitespaceIndex(5), e); // 8 + assert.equal(linesLayout.getIdForWhitespaceIndex(6), g); // 10 + assert.equal(linesLayout.getIdForWhitespaceIndex(7), f); // 11 + }); }); diff --git a/src/vs/editor/test/common/viewLayout/whitespaceComputer.test.ts b/src/vs/editor/test/common/viewLayout/whitespaceComputer.test.ts deleted file mode 100644 index d9ade64f3a5..00000000000 --- a/src/vs/editor/test/common/viewLayout/whitespaceComputer.test.ts +++ /dev/null @@ -1,558 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { WhitespaceComputer } from 'vs/editor/common/viewLayout/whitespaceComputer'; - -suite('Editor ViewLayout - WhitespaceComputer', () => { - - test('WhitespaceComputer', () => { - - let whitespaceComputer = new WhitespaceComputer(); - - // Insert a whitespace after line number 2, of height 10 - let a = whitespaceComputer.insertWhitespace(2, 0, 10, 0); - // whitespaces: a(2, 10) - assert.equal(whitespaceComputer.getCount(), 1); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 10); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 10); - assert.equal(whitespaceComputer.getTotalHeight(), 10); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 10); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 10); - - // Insert a whitespace again after line number 2, of height 20 - let b = whitespaceComputer.insertWhitespace(2, 0, 20, 0); - // whitespaces: a(2, 10), b(2, 20) - assert.equal(whitespaceComputer.getCount(), 2); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 10); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 10); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 30); - assert.equal(whitespaceComputer.getTotalHeight(), 30); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 30); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 30); - - // Change last inserted whitespace height to 30 - whitespaceComputer.changeWhitespaceHeight(b, 30); - // whitespaces: a(2, 10), b(2, 30) - assert.equal(whitespaceComputer.getCount(), 2); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 10); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 30); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 10); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 40); - assert.equal(whitespaceComputer.getTotalHeight(), 40); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 40); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 40); - - // Remove last inserted whitespace - whitespaceComputer.removeWhitespace(b); - // whitespaces: a(2, 10) - assert.equal(whitespaceComputer.getCount(), 1); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 10); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 10); - assert.equal(whitespaceComputer.getTotalHeight(), 10); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 10); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 10); - - // Add a whitespace before the first line of height 50 - b = whitespaceComputer.insertWhitespace(0, 0, 50, 0); - // whitespaces: b(0, 50), a(2, 10) - assert.equal(whitespaceComputer.getCount(), 2); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 50); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 10); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 50); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 60); - assert.equal(whitespaceComputer.getTotalHeight(), 60); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 60); - - // Add a whitespace after line 4 of height 20 - whitespaceComputer.insertWhitespace(4, 0, 20, 0); - // whitespaces: b(0, 50), a(2, 10), c(4, 20) - assert.equal(whitespaceComputer.getCount(), 3); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 50); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 10); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 4); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(2), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 50); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 60); - assert.equal(whitespaceComputer.getAccumulatedHeight(2), 80); - assert.equal(whitespaceComputer.getTotalHeight(), 80); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 60); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(5), 80); - - // Add a whitespace after line 3 of height 30 - whitespaceComputer.insertWhitespace(3, 0, 30, 0); - // whitespaces: b(0, 50), a(2, 10), d(3, 30), c(4, 20) - assert.equal(whitespaceComputer.getCount(), 4); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 50); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 10); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 3); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(2), 30); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(3), 4); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(3), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 50); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 60); - assert.equal(whitespaceComputer.getAccumulatedHeight(2), 90); - assert.equal(whitespaceComputer.getAccumulatedHeight(3), 110); - assert.equal(whitespaceComputer.getTotalHeight(), 110); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 60); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 90); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(5), 110); - - // Change whitespace after line 2 to height of 100 - whitespaceComputer.changeWhitespaceHeight(a, 100); - // whitespaces: b(0, 50), a(2, 100), d(3, 30), c(4, 20) - assert.equal(whitespaceComputer.getCount(), 4); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 50); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 100); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 3); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(2), 30); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(3), 4); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(3), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 50); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 150); - assert.equal(whitespaceComputer.getAccumulatedHeight(2), 180); - assert.equal(whitespaceComputer.getAccumulatedHeight(3), 200); - assert.equal(whitespaceComputer.getTotalHeight(), 200); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 150); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 180); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(5), 200); - - // Remove whitespace after line 2 - whitespaceComputer.removeWhitespace(a); - // whitespaces: b(0, 50), d(3, 30), c(4, 20) - assert.equal(whitespaceComputer.getCount(), 3); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 50); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 30); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 4); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(2), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 50); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 80); - assert.equal(whitespaceComputer.getAccumulatedHeight(2), 100); - assert.equal(whitespaceComputer.getTotalHeight(), 100); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 80); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(5), 100); - - // Remove whitespace before line 1 - whitespaceComputer.removeWhitespace(b); - // whitespaces: d(3, 30), c(4, 20) - assert.equal(whitespaceComputer.getCount(), 2); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 30); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 4); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 30); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 50); - assert.equal(whitespaceComputer.getTotalHeight(), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 30); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(5), 50); - - // Delete line 1 - whitespaceComputer.onLinesDeleted(1, 1); - // whitespaces: d(2, 30), c(3, 20) - assert.equal(whitespaceComputer.getCount(), 2); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 30); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 30); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 50); - assert.equal(whitespaceComputer.getTotalHeight(), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 30); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(5), 50); - - // Insert a line before line 1 - whitespaceComputer.onLinesInserted(1, 1); - // whitespaces: d(3, 30), c(4, 20) - assert.equal(whitespaceComputer.getCount(), 2); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 30); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 4); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 30); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 50); - assert.equal(whitespaceComputer.getTotalHeight(), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 30); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(5), 50); - - // Delete line 4 - whitespaceComputer.onLinesDeleted(4, 4); - // whitespaces: d(3, 30), c(3, 20) - assert.equal(whitespaceComputer.getCount(), 2); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(0), 30); - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(whitespaceComputer.getHeightForWhitespaceIndex(1), 20); - assert.equal(whitespaceComputer.getAccumulatedHeight(0), 30); - assert.equal(whitespaceComputer.getAccumulatedHeight(1), 50); - assert.equal(whitespaceComputer.getTotalHeight(), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(1), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(2), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(3), 0); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(4), 50); - assert.equal(whitespaceComputer.getAccumulatedHeightBeforeLineNumber(5), 50); - }); - - test('WhitespaceComputer findInsertionIndex', () => { - - let makeArray = (size: number, fillValue: number) => { - let r: number[] = []; - for (let i = 0; i < size; i++) { - r[i] = fillValue; - } - return r; - }; - - let arr: number[]; - let ordinals: number[]; - - arr = []; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 0); - - arr = [1]; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - - arr = [1, 3]; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 3, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 4, ordinals, 0), 2); - - arr = [1, 3, 5]; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 3, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 4, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 5, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 6, ordinals, 0), 3); - - arr = [1, 3, 5]; - ordinals = makeArray(arr.length, 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 3, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 4, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 5, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 6, ordinals, 0), 3); - - arr = [1, 3, 5, 7]; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 3, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 4, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 5, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 6, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 7, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 8, ordinals, 0), 4); - - arr = [1, 3, 5, 7, 9]; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 3, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 4, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 5, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 6, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 7, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 8, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 9, ordinals, 0), 5); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 10, ordinals, 0), 5); - - arr = [1, 3, 5, 7, 9, 11]; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 3, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 4, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 5, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 6, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 7, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 8, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 9, ordinals, 0), 5); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 10, ordinals, 0), 5); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 11, ordinals, 0), 6); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 12, ordinals, 0), 6); - - arr = [1, 3, 5, 7, 9, 11, 13]; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 3, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 4, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 5, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 6, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 7, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 8, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 9, ordinals, 0), 5); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 10, ordinals, 0), 5); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 11, ordinals, 0), 6); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 12, ordinals, 0), 6); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 13, ordinals, 0), 7); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 14, ordinals, 0), 7); - - arr = [1, 3, 5, 7, 9, 11, 13, 15]; - ordinals = makeArray(arr.length, 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 0, ordinals, 0), 0); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 1, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 2, ordinals, 0), 1); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 3, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 4, ordinals, 0), 2); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 5, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 6, ordinals, 0), 3); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 7, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 8, ordinals, 0), 4); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 9, ordinals, 0), 5); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 10, ordinals, 0), 5); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 11, ordinals, 0), 6); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 12, ordinals, 0), 6); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 13, ordinals, 0), 7); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 14, ordinals, 0), 7); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 15, ordinals, 0), 8); - assert.equal(WhitespaceComputer.findInsertionIndex(arr, 16, ordinals, 0), 8); - }); - - test('WhitespaceComputer changeWhitespaceAfterLineNumber & getFirstWhitespaceIndexAfterLineNumber', () => { - let whitespaceComputer = new WhitespaceComputer(); - - let a = whitespaceComputer.insertWhitespace(0, 0, 1, 0); - let b = whitespaceComputer.insertWhitespace(7, 0, 1, 0); - let c = whitespaceComputer.insertWhitespace(3, 0, 1, 0); - - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 0); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 7); - - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(1), 1); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- - - // Do not really move a - whitespaceComputer.changeWhitespaceAfterLineNumber(a, 1); - - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 1 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 1); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 7); - - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(2), 1); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- - - - // Do not really move a - whitespaceComputer.changeWhitespaceAfterLineNumber(a, 2); - - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 2 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 2); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 7); - - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(3), 1); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- - - - // Change a to conflict with c => a gets placed after c - whitespaceComputer.changeWhitespaceAfterLineNumber(a, 3); - - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), c); // 3 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), a); // 3 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 7); - - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- - - - // Make a no-op - whitespaceComputer.changeWhitespaceAfterLineNumber(c, 3); - - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), c); // 3 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), a); // 3 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 3); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), b); // 7 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 7); - - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(1), 0); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(2), 0); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(3), 0); // c - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(4), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(5), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(6), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(7), 2); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- - - - - // Conflict c with b => c gets placed after b - whitespaceComputer.changeWhitespaceAfterLineNumber(c, 7); - - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 3 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(0), 3); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), b); // 7 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(1), 7); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), c); // 7 - assert.equal(whitespaceComputer.getAfterLineNumberForWhitespaceIndex(2), 7); - - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(1), 0); // a - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(2), 0); // a - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(3), 0); // a - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(4), 1); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(5), 1); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(6), 1); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(7), 1); // b - assert.equal(whitespaceComputer.getFirstWhitespaceIndexAfterLineNumber(8), -1); // -- - }); - - - test('WhitespaceComputer Bug', () => { - let whitespaceComputer = new WhitespaceComputer(); - - let a = whitespaceComputer.insertWhitespace(0, 0, 1, 0); - let b = whitespaceComputer.insertWhitespace(7, 0, 1, 0); - - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), b); // 7 - - let c = whitespaceComputer.insertWhitespace(3, 0, 1, 0); - - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), c); // 3 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), b); // 7 - - let d = whitespaceComputer.insertWhitespace(2, 0, 1, 0); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(3), b); // 7 - - let e = whitespaceComputer.insertWhitespace(8, 0, 1, 0); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(4), e); // 8 - - let f = whitespaceComputer.insertWhitespace(11, 0, 1, 0); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(4), e); // 8 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(5), f); // 11 - - let g = whitespaceComputer.insertWhitespace(10, 0, 1, 0); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), d); // 2 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), c); // 3 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(3), b); // 7 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(4), e); // 8 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(5), g); // 10 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(6), f); // 11 - - let h = whitespaceComputer.insertWhitespace(0, 0, 1, 0); - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(0), a); // 0 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(1), h); // 0 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(2), d); // 2 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(3), c); // 3 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(4), b); // 7 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(5), e); // 8 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(6), g); // 10 - assert.equal(whitespaceComputer.getIdForWhitespaceIndex(7), f); // 11 - }); -}); - diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 17dfd5efde6..05efd4bd424 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -236,28 +236,52 @@ export class MenuEntryActionViewItem extends ActionViewItem { _updateItemClass(item: ICommandAction): void { this._itemClassDispose.value = undefined; - if (item.iconLocation) { - let iconClass: string; - - const iconPathMapKey = item.iconLocation.dark.toString(); - - if (MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { - iconClass = MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; - } else { - iconClass = ids.nextId(); - createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(item.iconLocation.light || item.iconLocation.dark)}`); - createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(item.iconLocation.dark)}`); - MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); - } + // icon class + if (item.iconClassName) { + let iconClass = item.iconClassName; if (this.label) { - addClasses(this.label, 'icon', iconClass); + addClasses(this.label, 'codicon', iconClass); this._itemClassDispose.value = toDisposable(() => { if (this.label) { - removeClasses(this.label, 'icon', iconClass); + removeClasses(this.label, 'codicon', iconClass); } }); } + + } + + // icon path + else if (item.iconLocation) { + let iconClass: string; + + if (item.iconLocation?.dark?.scheme) { + + const iconPathMapKey = item.iconLocation.dark.toString(); + + if (MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.has(iconPathMapKey)) { + iconClass = MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.get(iconPathMapKey)!; + } else { + iconClass = ids.nextId(); + createCSSRule(`.icon.${iconClass}`, `background-image: ${asCSSUrl(item.iconLocation.light || item.iconLocation.dark)}`); + createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: ${asCSSUrl(item.iconLocation.dark)}`); + MenuEntryActionViewItem.ICON_PATH_TO_CSS_RULES.set(iconPathMapKey, iconClass); + } + + if (this.label) { + + addClasses(this.label, 'icon', iconClass); + this._itemClassDispose.value = toDisposable(() => { + if (this.label) { + removeClasses(this.label, 'icon', iconClass); + } + }); + } + + } + + + } } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 2cea44d8589..cc7b367fd5d 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -22,7 +22,8 @@ export interface ICommandAction { id: string; title: string | ILocalizedString; category?: string | ILocalizedString; - iconLocation?: { dark: URI; light?: URI; }; + iconClassName?: string; + iconLocation?: { dark?: URI; light?: URI; }; precondition?: ContextKeyExpr; toggled?: ContextKeyExpr; } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 945da642232..96031eb4ec7 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -86,6 +86,7 @@ export interface ParsedArgs { 'disable-gpu'?: boolean; 'nolazy'?: boolean; 'force-device-scale-factor'?: string; + 'force-renderer-accessibility'?: boolean; } export const IEnvironmentService = createDecorator('environmentService'); diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index b97cc4b20b1..6832b93c5cb 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -118,6 +118,7 @@ export const OPTIONS: OptionDescriptions> = { 'inspect-brk': { type: 'string' }, 'nolazy': { type: 'boolean' }, // node inspect 'force-device-scale-factor': { type: 'string' }, + 'force-renderer-accessibility': { type: 'boolean' }, '_urls': { type: 'string[]' }, _: { type: 'string[]' } // main arguments diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index a36425204f7..5bfc2bb66c1 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -217,6 +217,16 @@ export class ExtensionManagementService extends Disposable implements IExtension } else if (semver.gt(existing.manifest.version, manifest.version)) { return this.uninstall(existing, true); } + } else { + // Remove the extension with same version if it is already uninstalled. + // Installing a VSIX extension shall replace the existing extension always. + return this.unsetUninstalledAndGetLocal(identifierWithVersion) + .then(existing => { + if (existing) { + return this.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)))); + } + return undefined; + }); } return undefined; }) diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 3b60521677a..8f7921d1581 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -6,6 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings'; export const IOpenerService = createDecorator('openerService'); @@ -35,8 +36,7 @@ export interface IResolvedExternalUri extends IDisposable { } export interface IOpener { - open(resource: URI, options?: OpenInternalOptions): Promise; - open(resource: URI, options?: OpenExternalOptions): Promise; + open(resource: URI | string, options?: OpenInternalOptions | OpenExternalOptions): Promise; } export interface IExternalOpener { @@ -44,7 +44,7 @@ export interface IExternalOpener { } export interface IValidator { - shouldOpen(resource: URI): Promise; + shouldOpen(resource: URI | string): Promise; } export interface IExternalUriResolver { @@ -83,8 +83,7 @@ export interface IOpenerService { * @param resource A resource * @return A promise that resolves when the opening is done. */ - open(resource: URI, options?: OpenInternalOptions): Promise; - open(resource: URI, options?: OpenExternalOptions): Promise; + open(resource: URI | string, options?: OpenInternalOptions | OpenExternalOptions): Promise; /** * Resolve a resource to its external form. @@ -101,3 +100,11 @@ export const NullOpenerService: IOpenerService = Object.freeze({ async open() { return false; }, async resolveExternalUri(uri: URI) { return { resolved: uri, dispose() { } }; }, }); + +export function matchesScheme(target: URI | string, scheme: string) { + if (URI.isUri(target)) { + return equalsIgnoreCase(target.scheme, scheme); + } else { + return startsWithIgnoreCase(target, scheme + ':'); + } +} diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index b526d149fa3..eab85914921 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -89,8 +89,8 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio options.host, options.port, `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, - (err: any, socket: ISocket) => { - if (err) { + (err: any, socket: ISocket | undefined) => { + if (err || !socket) { options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); options.logService.error(err); e(err); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 9d00a3150b1..4eed2e45d76 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2009,8 +2009,9 @@ declare module 'vscode' { /** * Base kind for source actions: `source` * - * Source code actions apply to the entire file and can be run on save - * using `editor.codeActionsOnSave`. They also are shown in `source` context menu. + * Source code actions apply to the entire file. They must be explicitly requested and will not show in the + * normal [light bulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) menu. Source actions + * can be run on save using `editor.codeActionsOnSave` and are also shown in the `source` context menu. */ static readonly Source: CodeActionKind; diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 3e79332f086..023e413f8cc 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -30,7 +30,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { ISaveParticipant, SaveReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { ISaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; import { ExtHostContext, ExtHostDocumentSaveParticipantShape, IExtHostContext } from '../common/extHost.protocol'; export interface ICodeActionsOnSaveOptions { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 29b532253ec..29fe210a1b6 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -12,7 +12,6 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -21,7 +20,7 @@ import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } fr import { IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; -import { CustomEditorModel } from 'vs/workbench/contrib/customEditor/browser/customEditorModel'; +import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; @@ -96,14 +95,13 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private readonly _webviewInputs = new WebviewInputStore(); private readonly _revivers = new Map(); private readonly _editorProviders = new Map(); - private readonly _models = new Map(); constructor( context: extHostProtocol.IExtHostContext, @IExtensionService extensionService: IExtensionService, + @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IOpenerService private readonly _openerService: IOpenerService, @IProductService private readonly _productService: IProductService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -273,18 +271,20 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.webview.options = options; webviewInput.webview.extension = extension; - const model = this._instantiationService.createInstance(CustomEditorModel, webviewInput.getResource()); - webviewInput.setModel(model); - this._models.set(handle, model); - - webviewInput.onDispose(() => { - this._models.delete(handle); - }); + const model = await this._customEditorService.models.loadOrCreate(webviewInput.getResource(), webviewInput.viewType); model.onUndo(edit => { this._proxy.$undoEdits(handle, [edit]); }); + model.onRedo(edit => { + this._proxy.$redoEdits(handle, [edit]); + }); + + webviewInput.onDispose(() => { + this._customEditorService.models.disposeModel(model); + }); + try { await this._proxy.$resolveWebviewEditor( webviewInput.getResource(), @@ -318,7 +318,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma throw new Error('Webview is not a webview editor'); } - const model = this._models.get(handle); + const model = this._customEditorService.models.get(webview.getResource(), webview.viewType); if (!model) { throw new Error('Could not find model for webview editor'); } diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index bbce19a3388..2a431111e93 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -42,9 +42,17 @@ export class MainThreadWindow implements MainThreadWindowShape { return Promise.resolve(this.hostService.hasFocus); } - async $openUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { + async $openUri(uriComponents: UriComponents, uriString: string | undefined, options: IOpenUriOptions): Promise { const uri = URI.from(uriComponents); - return this.openerService.open(uri, { openExternal: true, allowTunneling: options.allowTunneling }); + let target: URI | string; + if (uriString && URI.parse(uriString).toString() === uri.toString()) { + // called with string and no transformation happened -> keep string + target = uriString; + } else { + // called with URI or transformed -> use uri + target = uri; + } + return this.openerService.open(target, { openExternal: true, allowTunneling: options.allowTunneling }); } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index b41e21a9cad..1ad44491aa1 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -15,7 +15,7 @@ import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'v import { IWorkspaceContextService, WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape, IWorkspaceData, ITextSearchComplete } from '../common/extHost.protocol'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -37,7 +37,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { extHostContext: IExtHostContext, @ISearchService private readonly _searchService: ISearchService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - @ITextFileService private readonly _textFileService: ITextFileService, + @IEditorService private readonly _editorService: IEditorService, @IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService, @INotificationService private readonly _notificationService: INotificationService, @IRequestService private readonly _requestService: IRequestService, @@ -212,9 +212,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- save & edit resources --- $saveAll(includeUntitled?: boolean): Promise { - return this._textFileService.saveAll(includeUntitled).then(result => { - return result.results.every(each => each.success === true); - }); + return this._editorService.saveAll({ includeUntitled }); } $resolveProxy(url: string): Promise { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9e25546cc68..93d36038334 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -185,8 +185,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return activeTextEditor.edit((edit: vscode.TextEditorEdit) => { - args.unshift(activeTextEditor, edit); - callback.apply(thisArg, args); + callback.apply(thisArg, [activeTextEditor, edit, ...args]); }).then((result) => { if (!result) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 1cdb4642c86..87349faf1cb 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -45,7 +45,7 @@ import { ITerminalDimensions, IShellLaunchConfig } from 'vs/workbench/contrib/te import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; -import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; export interface IEnvironment { @@ -591,6 +591,7 @@ export interface ExtHostWebviewsShape { $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; $undoEdits(handle: WebviewPanelHandle, edits: string[]): void; + $redoEdits(handle: WebviewPanelHandle, edits: string[]): void; } export interface MainThreadUrlsShape extends IDisposable { @@ -760,7 +761,7 @@ export interface IOpenUriOptions { export interface MainThreadWindowShape extends IDisposable { $getWindowVisibility(): Promise; - $openUri(uri: UriComponents, options: IOpenUriOptions): Promise; + $openUri(uri: UriComponents, uriString: string | undefined, options: IOpenUriOptions): Promise; $asExternalUri(uri: UriComponents, options: IOpenUriOptions): Promise; } diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index f55a89b3f30..63f885cc69a 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -11,7 +11,7 @@ import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IResou import { TextEdit } from 'vs/workbench/api/common/extHostTypes'; import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; import * as vscode from 'vscode'; import { LinkedList } from 'vs/base/common/linkedList'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index b6f94048fbd..c06e86c8305 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -245,9 +245,9 @@ class OpenNodeModuleFactory implements INodeModuleFactory { return this.callOriginal(target, options); } if (uri.scheme === 'http' || uri.scheme === 'https') { - return mainThreadWindow.$openUri(uri, { allowTunneling: true }); + return mainThreadWindow.$openUri(uri, target, { allowTunneling: true }); } else if (uri.scheme === 'mailto' || uri.scheme === this._appUriScheme) { - return mainThreadWindow.$openUri(uri, {}); + return mainThreadWindow.$openUri(uri, target, {}); } return this.callOriginal(target, options); }; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index e0363039ace..74f6b5d98a4 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -9,7 +9,7 @@ import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShap import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { EXT_HOST_CREATION_DELAY, ITerminalChildProcess, ITerminalDimensions } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalChildProcess, ITerminalDimensions, EXT_HOST_CREATION_DELAY } from 'vs/workbench/contrib/terminal/common/terminal'; import { timeout } from 'vs/base/common/async'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; @@ -437,22 +437,6 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ } } - public performTerminalIdAction(id: number, callback: (terminal: ExtHostTerminal) => void): void { - // TODO: Use await this._getTerminalByIdEventually(id); - let terminal = this._getTerminalById(id); - if (terminal) { - callback(terminal); - } else { - // Retry one more time in case the terminal has not yet been initialized. - setTimeout(() => { - terminal = this._getTerminalById(id); - if (terminal) { - callback(terminal); - } - }, EXT_HOST_CREATION_DELAY * 2); - } - } - public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise { // Make sure the ExtHostTerminal exists so onDidOpenTerminal has fired before we call // Pseudoterminal.start @@ -550,10 +534,6 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ private _getTerminalByIdEventually(id: number, retries: number = 5): Promise { if (!this._getTerminalPromises[id]) { this._getTerminalPromises[id] = this._createGetTerminalPromise(id, retries); - } else { - this._getTerminalPromises[id].then(c => { - return this._createGetTerminalPromise(id, retries); - }); } return this._getTerminalPromises[id]; } @@ -571,7 +551,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ } else { // This should only be needed immediately after createTerminalRenderer is called as // the ExtHostTerminal has not yet been iniitalized - timeout(200).then(() => c(this._createGetTerminalPromise(id, retries - 1))); + timeout(EXT_HOST_CREATION_DELAY * 2).then(() => c(this._createGetTerminalPromise(id, retries - 1))); } }); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index a0671a6e51e..08cfac73625 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -13,7 +13,7 @@ import { EndOfLineSequence, TrackedRangeStickiness } from 'vs/editor/common/mode import * as vscode from 'vscode'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress'; -import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; import { IPosition } from 'vs/editor/common/core/position'; import * as editorRange from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; @@ -31,7 +31,6 @@ import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; - export interface PositionLike { line: number; character: number; diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index e13466a070d..1fc1214a5e5 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -252,6 +252,10 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa assertIsDefined(this._capabilities).editingCapability?.undoEdits(edits); } + _redoEdits(edits: string[]): void { + assertIsDefined(this._capabilities).editingCapability?.applyEdits(edits); + } + private assertNotDisposed() { if (this._isDisposed) { throw new Error('Webview is disposed'); @@ -447,6 +451,14 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { panel._undoEdits(edits); } + $redoEdits(handle: WebviewPanelHandle, edits: string[]): void { + const panel = this.getWebviewPanel(handle); + if (!panel) { + return; + } + panel._redoEdits(edits); + } + private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { return this._webviewPanels.get(handle); } diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index 8ce82ac8b2d..f8c5d5fa3d7 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -39,7 +39,9 @@ export class ExtHostWindow implements ExtHostWindowShape { } openUri(stringOrUri: string | URI, options: IOpenUriOptions): Promise { + let uriAsString: string | undefined; if (typeof stringOrUri === 'string') { + uriAsString = stringOrUri; try { stringOrUri = URI.parse(stringOrUri); } catch (e) { @@ -51,7 +53,7 @@ export class ExtHostWindow implements ExtHostWindowShape { } else if (stringOrUri.scheme === Schemas.command) { return Promise.reject(`Invalid scheme '${stringOrUri.scheme}'`); } - return this._proxy.$openUri(stringOrUri, options); + return this._proxy.$openUri(stringOrUri, uriAsString, options); } async asExternalUri(uri: URI, options: IOpenUriOptions): Promise { diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index bffcbcddd2b..81e40b7f893 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -38,6 +38,7 @@ import { ISignService } from 'vs/platform/sign/common/sign'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService'; +import { withNullAsUndefined } from 'vs/base/common/types'; export class ExtHostDebugService implements IExtHostDebugService, ExtHostDebugServiceShape { @@ -114,7 +115,7 @@ export class ExtHostDebugService implements IExtHostDebugService, ExtHostDebugSe this._onDidStartDebugSession = new Emitter(); this._onDidTerminateDebugSession = new Emitter(); - this._onDidChangeActiveDebugSession = new Emitter(); + this._onDidChangeActiveDebugSession = new Emitter(); this._onDidReceiveDebugSessionCustomEvent = new Emitter(); this._debugServiceProxy = extHostRpcService.getProxy(MainContext.MainThreadDebugService); @@ -511,11 +512,11 @@ export class ExtHostDebugService implements IExtHostDebugService, ExtHostDebugSe } this._debugServiceProxy.$acceptDAError(debugAdapterHandle, err.name, err.message, err.stack); }); - debugAdapter.onExit((code: number) => { + debugAdapter.onExit((code: number | null) => { if (tracker && tracker.onExit) { - tracker.onExit(code, undefined); + tracker.onExit(withNullAsUndefined(code), undefined); } - this._debugServiceProxy.$acceptDAExit(debugAdapterHandle, code, undefined); + this._debugServiceProxy.$acceptDAExit(debugAdapterHandle, withNullAsUndefined(code), undefined); }); if (tracker && tracker.onWillStartSession) { diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index b129dc792f8..c8941a826a1 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -22,7 +22,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesService, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; export class OpenFileAction extends Action { @@ -213,7 +213,7 @@ export class SaveWorkspaceAsAction extends Action { async run(): Promise { const configPathUri = await this.workspaceEditingService.pickNewWorkspacePath(); - if (configPathUri) { + if (configPathUri && hasWorkspaceFileExtension(configPathUri)) { switch (this.contextService.getWorkbenchState()) { case WorkbenchState.EMPTY: case WorkbenchState.FOLDER: diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index d45fe4e74e7..66d6d8138a6 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; import { IWindowsConfiguration } from 'vs/platform/windows/common/windows'; -import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorIsSaveableContext, toResource, SideBySideEditor, EditorAreaVisibleContext } from 'vs/workbench/common/editor'; +import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorIsSaveableContext, EditorAreaVisibleContext, DirtyWorkingCopiesContext } from 'vs/workbench/common/editor'; import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -21,8 +21,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; import { PanelPositionContext } from 'vs/workbench/common/panel'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; -import { IFileService } from 'vs/platform/files/common/files'; -import { Schemas } from 'vs/base/common/network'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const IsMacContext = new RawContextKey('isMac', isMacintosh); export const IsLinuxContext = new RawContextKey('isLinux', isLinux); @@ -51,6 +50,8 @@ export const IsFullscreenContext = new RawContextKey('isFullscreen', fa export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; + private dirtyWorkingCopiesContext: IContextKey; + private activeEditorContext: IContextKey; private activeEditorIsSaveable: IContextKey; @@ -75,19 +76,18 @@ export class WorkbenchContextKeysHandler extends Disposable { private panelPositionContext: IContextKey; constructor( - @IContextKeyService private contextKeyService: IContextKeyService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IConfigurationService private configurationService: IConfigurationService, - @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, - @IEditorService private editorService: IEditorService, - @IEditorGroupsService private editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService, - @IViewletService private viewletService: IViewletService, - @IFileService private fileService: IFileService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IViewletService private readonly viewletService: IViewletService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService ) { super(); - // Platform IsMacContext.bindTo(this.contextKeyService); IsLinuxContext.bindTo(this.contextKeyService); @@ -116,6 +116,9 @@ export class WorkbenchContextKeysHandler extends Disposable { this.activeEditorGroupLast = ActiveEditorGroupLastContext.bindTo(this.contextKeyService); this.multipleEditorGroupsContext = MultipleEditorGroupsContext.bindTo(this.contextKeyService); + // Working Copies + this.dirtyWorkingCopiesContext = DirtyWorkingCopiesContext.bindTo(this.contextKeyService); + // Inputs this.inputFocusedContext = InputFocusedContext.bindTo(this.contextKeyService); @@ -183,6 +186,8 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.viewletService.onDidViewletOpen(() => this.updateSideBarContextKeys())); this._register(this.layoutService.onPartVisibilityChange(() => this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART)))); + + this._register(this.workingCopyService.onDidChangeDirty(w => this.dirtyWorkingCopiesContext.set(w.isDirty() || this.workingCopyService.hasDirty))); } private updateEditorContextKeys(): void { @@ -217,10 +222,7 @@ export class WorkbenchContextKeysHandler extends Disposable { if (activeControl) { this.activeEditorContext.set(activeControl.getId()); - - const resource = toResource(activeControl.input, { supportSideBySide: SideBySideEditor.MASTER }); - const canSave = resource ? this.fileService.canHandleResource(resource) || resource.scheme === Schemas.untitled : false; - this.activeEditorIsSaveable.set(canSave); + this.activeEditorIsSaveable.set(!activeControl.input.isReadonly()); } else { this.activeEditorContext.reset(); this.activeEditorIsSaveable.reset(); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index d6c63962f8e..51d6ed18b7c 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -28,7 +28,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; export interface IResourceLabelProps { resource?: URI; - name?: string; + name?: string | string[]; description?: string; } @@ -41,6 +41,7 @@ export interface IResourceLabelOptions extends IIconLabelValueOptions { export interface IFileLabelOptions extends IResourceLabelOptions { hideLabel?: boolean; hidePath?: boolean; + readonly parentCount?: number; } export interface IResourceLabel extends IDisposable { @@ -442,7 +443,8 @@ class ResourceLabelWidget extends IconLabel { title: '', italic: this.options && this.options.italic, matches: this.options && this.options.matches, - extraClasses: [] + extraClasses: [], + separator: this.options?.separator }; const resource = this.label.resource; diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 0f6da876c7b..d087cd74997 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -44,6 +44,10 @@ body.web { position: fixed; /* prevent bounce effect */ } +.monaco-workbench.web { + touch-action: initial; /* reenable touch events on workbench */ +} + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .monaco-workbench { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 15acfe38cd9..fdcf9b346d1 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -22,7 +22,7 @@ import { ActivityAction, ActivityActionViewItem, ICompositeBar, ICompositeBarCol import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { IActivity } from 'vs/workbench/common/activity'; -import { ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; +import { ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_ACTIVE_FOCUS_BORDER, ACTIVITY_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -297,6 +297,20 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { `); } + const activeFocusBorderColor = theme.getColor(ACTIVITY_BAR_ACTIVE_FOCUS_BORDER); + if (activeFocusBorderColor) { + collector.addRule(` + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:focus::before { + visibility: hidden; + } + + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:focus .active-item-indicator:before { + visibility: visible; + border-left-color: ${activeFocusBorderColor}; + } + `); + } + const activeBackgroundColor = theme.getColor(ACTIVITY_BAR_ACTIVE_BACKGROUND); if (activeBackgroundColor) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 1fb755062de..28c43d3ab7a 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -6,14 +6,15 @@ .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item { display: block; position: relative; - padding: 5px 0; + margin-bottom: 4px; } + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-label { position: relative; z-index: 1; display: flex; overflow: hidden; - height: 40px; + height: 48px; margin-right: 0; box-sizing: border-box; @@ -35,11 +36,10 @@ .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before { content: ""; position: absolute; - top: 9px; - height: 32px; + top: 0; z-index: 1; - top: 5px; - height: 40px; + top: 0; + height: 100%; width: 0; border-left: 2px solid; } @@ -50,7 +50,7 @@ } .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:focus .active-item-indicator:before { - border-left: none; /* don't show active border + focus at the same time, focus takes priority */ + visibility: hidden; /* don't show active border + focus at the same time, focus takes priority */ } /* Hides active elements in high contrast mode */ @@ -62,20 +62,14 @@ border-left: none !important; /* no focus feedback when using mouse */ } +.monaco-workbench .activitybar.left > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before, .monaco-workbench .activitybar.left > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .active-item-indicator:before{ left: 0; } -.monaco-workbench .activitybar.left > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before { - left: 1px; -} - +.monaco-workbench .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .active-item-indicator:before, .monaco-workbench .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before { - right: 1px; -} - -.monaco-workbench .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .active-item-indicator:before { - right: 2px; + right: 0; } /* Hides outline on HC as focus is handled by border */ @@ -88,16 +82,18 @@ .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .badge { position: absolute; z-index: 1; - top: 5px; + top: 0; + bottom: 0; + margin: auto; left: 0; overflow: hidden; - width: 50px; - height: 40px; + width: 100%; + height: 100%; } .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .badge .badge-content { position: absolute; - top: 20px; + top: 24px; right: 8px; font-size: 9px; font-weight: 600; @@ -113,7 +109,7 @@ .monaco-workbench .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-label:not(.codicon) { margin-left: 0; - padding: 0 50px 0 0; + padding: 0 48px 0 0; background-position: calc(100% - 9px) center; } diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 0c2e0156455..24e6f4bc58c 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -15,7 +15,6 @@ import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { CLOSE_EDITOR_COMMAND_ID, NAVIGATE_ALL_EDITORS_GROUP_PREFIX, MOVE_ACTIVE_EDITOR_COMMAND_ID, NAVIGATE_IN_ACTIVE_GROUP_PREFIX, ActiveEditorMoveArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, mergeAllGroups } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, EditorsOrder, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -507,7 +506,7 @@ export class CloseOneEditorAction extends Action { // Close specific editor in group if (typeof editorIndex === 'number') { - const editorAtIndex = group.getEditor(editorIndex); + const editorAtIndex = group.getEditorByIndex(editorIndex); if (editorAtIndex) { return group.closeEditor(editorAtIndex); } @@ -598,10 +597,10 @@ export abstract class BaseCloseAllAction extends Action { id: string, label: string, clazz: string | undefined, - private textFileService: ITextFileService, private workingCopyService: IWorkingCopyService, private fileDialogService: IFileDialogService, - protected editorGroupService: IEditorGroupsService + protected editorGroupService: IEditorGroupsService, + private editorService: IEditorService ) { super(id, label, clazz); } @@ -647,11 +646,10 @@ export abstract class BaseCloseAllAction extends Action { let saveOrRevert: boolean; if (confirm === ConfirmResult.DONT_SAVE) { - await this.textFileService.revertAll(undefined, { soft: true }); + await this.editorService.revertAll({ soft: true }); saveOrRevert = true; } else { - const res = await this.textFileService.saveAll(true); - saveOrRevert = res.results.every(r => !!r.success); + saveOrRevert = await this.editorService.saveAll({ includeUntitled: true }); } if (saveOrRevert) { @@ -670,12 +668,12 @@ export class CloseAllEditorsAction extends BaseCloseAllAction { constructor( id: string, label: string, - @ITextFileService textFileService: ITextFileService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IFileDialogService fileDialogService: IFileDialogService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService ) { - super(id, label, 'codicon-close-all', textFileService, workingCopyService, fileDialogService, editorGroupService); + super(id, label, 'codicon-close-all', workingCopyService, fileDialogService, editorGroupService, editorService); } protected doCloseAll(): Promise { @@ -691,12 +689,12 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction { constructor( id: string, label: string, - @ITextFileService textFileService: ITextFileService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IFileDialogService fileDialogService: IFileDialogService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService ) { - super(id, label, undefined, textFileService, workingCopyService, fileDialogService, editorGroupService); + super(id, label, undefined, workingCopyService, fileDialogService, editorGroupService, editorService); } protected async doCloseAll(): Promise { diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 38bdcd700ce..682dbaedff1 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -318,7 +318,7 @@ function registerOpenEditorAtIndexCommands(): void { const editorService = accessor.get(IEditorService); const activeControl = editorService.activeControl; if (activeControl) { - const editor = activeControl.group.getEditor(editorIndex); + const editor = activeControl.group.getEditorByIndex(editorIndex); if (editor) { editorService.openEditor(editor); } @@ -448,7 +448,7 @@ export function splitEditor(editorGroupService: IEditorGroupsService, direction: // Split editor (if it can be split) let editorToCopy: IEditorInput | undefined; if (context && typeof context.editorIndex === 'number') { - editorToCopy = sourceGroup.getEditor(context.editorIndex); + editorToCopy = sourceGroup.getEditorByIndex(context.editorIndex); } else { editorToCopy = types.withNullAsUndefined(sourceGroup.activeEditor); } @@ -548,7 +548,7 @@ function registerCloseEditorCommands() { if (group) { const editors = coalesce(contexts .filter(context => context.groupId === groupId) - .map(context => typeof context.editorIndex === 'number' ? group.getEditor(context.editorIndex) : group.activeEditor)); + .map(context => typeof context.editorIndex === 'number' ? group.getEditorByIndex(context.editorIndex) : group.activeEditor)); return group.closeEditors(editors); } @@ -603,7 +603,7 @@ function registerCloseEditorCommands() { if (group) { const editors = contexts .filter(context => context.groupId === groupId) - .map(context => typeof context.editorIndex === 'number' ? group.getEditor(context.editorIndex) : group.activeEditor); + .map(context => typeof context.editorIndex === 'number' ? group.getEditorByIndex(context.editorIndex) : group.activeEditor); const editorsToClose = group.editors.filter(e => editors.indexOf(e) === -1); if (group.activeEditor) { @@ -715,7 +715,7 @@ function resolveCommandsContext(editorGroupService: IEditorGroupsService, contex // Resolve from context let group = context && typeof context.groupId === 'number' ? editorGroupService.getGroup(context.groupId) : undefined; - let editor = group && context && typeof context.editorIndex === 'number' ? types.withNullAsUndefined(group.getEditor(context.editorIndex)) : undefined; + let editor = group && context && typeof context.editorIndex === 'number' ? types.withNullAsUndefined(group.getEditorByIndex(context.editorIndex)) : undefined; let control = group ? group.activeControl : undefined; // Fallback to active group as needed diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 01eab7e6856..dc5ce544297 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -518,11 +518,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { editorsToClose.push(editor.master, editor.details); } - // Close the editor when it is no longer open in any group including diff editors + // Dispose the editor when it is no longer open in any group including diff editors editorsToClose.forEach(editorToClose => { - const resource = editorToClose ? editorToClose.getResource() : undefined; // prefer resource to not close right-hand side editors of a diff editor - if (!this.accessor.groups.some(groupView => groupView.group.contains(resource || editorToClose))) { - editorToClose.close(); + if (!this.accessor.groups.some(groupView => groupView.group.contains(editorToClose, true /* include side by side editor master & details */))) { + editorToClose.dispose(); } }); @@ -761,8 +760,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.editors; } - getEditor(index: number): EditorInput | undefined { - return this._group.getEditor(index); + getEditorByIndex(index: number): EditorInput | undefined { + return this._group.getEditorByIndex(index); } getIndexOfEditor(editor: EditorInput): number { @@ -1121,7 +1120,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Check for dirty and veto - const veto = await this.handleDirty([editor]); + const veto = await this.handleDirtyClosing([editor]); if (veto) { return; } @@ -1232,7 +1231,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._group.closeEditor(editor); } - private async handleDirty(editors: EditorInput[]): Promise { + private async handleDirtyClosing(editors: EditorInput[]): Promise { if (!editors.length) { return false; // no veto } @@ -1241,13 +1240,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // To prevent multiple confirmation dialogs from showing up one after the other // we check if a pending confirmation is currently showing and if so, join that - let handleDirtyPromise = this.mapEditorToPendingConfirmation.get(editor); - if (!handleDirtyPromise) { - handleDirtyPromise = this.doHandleDirty(editor); - this.mapEditorToPendingConfirmation.set(editor, handleDirtyPromise); + let handleDirtyClosingPromise = this.mapEditorToPendingConfirmation.get(editor); + if (!handleDirtyClosingPromise) { + handleDirtyClosingPromise = this.doHandleDirtyClosing(editor); + this.mapEditorToPendingConfirmation.set(editor, handleDirtyClosingPromise); } - const veto = await handleDirtyPromise; + const veto = await handleDirtyClosingPromise; // Make sure to remove from our map of cached pending confirmations this.mapEditorToPendingConfirmation.delete(editor); @@ -1258,16 +1257,40 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Otherwise continue with the remainders - return this.handleDirty(editors); + return this.handleDirtyClosing(editors); } - private async doHandleDirty(editor: EditorInput): Promise { - if ( - !editor.isDirty() || // editor must be dirty - this.accessor.groups.some(groupView => groupView !== this && groupView.group.contains(editor, true /* support side by side */)) || // editor is opened in other group - editor instanceof SideBySideEditorInput && this.isOpened(editor.master) // side by side editor master is still opened - ) { + private async doHandleDirtyClosing(editor: EditorInput): Promise { + if (!editor.isDirty()) { + return false; // editor must be dirty + } + + if (editor instanceof SideBySideEditorInput && this.isOpened(editor.master)) { + return false; // master-side of editor is still opened somewhere else + } + + // Note: we explicitly decide to ask for confirm if closing a normal editor even + // if it is opened in a side-by-side editor in the group. This decision is made + // because it may be less obvious that one side of a side by side editor is dirty + // and can still be changed. + + if (this.accessor.groups.some(groupView => { + if (groupView === this) { + return false; // skip this group to avoid false assumptions about the editor being opened still + } + + const otherGroup = groupView.group; + if (otherGroup.contains(editor)) { + return true; // exact editor still opened + } + + if (editor instanceof SideBySideEditorInput && otherGroup.contains(editor.master)) { + return true; // master side of side by side editor still opened + } + return false; + })) { + return false; // editor is still editable somewhere else } // Switch to editor that we want to handle and confirm to save/revert @@ -1287,7 +1310,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Otherwise, handle accordingly switch (res) { case ConfirmResult.SAVE: - const result = await editor.save(); + const result = await editor.save(this._group.id); return !result; case ConfirmResult.DONT_SAVE: @@ -1324,7 +1347,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const editors = this.getEditorsToClose(args); // Check for dirty and veto - const veto = await this.handleDirty(editors.slice(0)); + const veto = await this.handleDirtyClosing(editors.slice(0)); if (veto) { return; } @@ -1403,7 +1426,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Check for dirty and veto const editors = this._group.getEditors(true); - const veto = await this.handleDirty(editors.slice(0)); + const veto = await this.handleDirtyClosing(editors.slice(0)); if (veto) { return; } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index d22c4b9ec41..d42242fe112 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -501,7 +501,7 @@ export class TabsTitleControl extends TitleControl { } // Open tabs editor - const input = this.group.getEditor(index); + const input = this.group.getEditorByIndex(index); if (input) { this.group.openEditor(input); } @@ -512,7 +512,7 @@ export class TabsTitleControl extends TitleControl { const showContextMenu = (e: Event) => { EventHelper.stop(e); - const input = this.group.getEditor(index); + const input = this.group.getEditorByIndex(index); if (input) { this.onContextMenu(input, e, tab); } @@ -562,7 +562,7 @@ export class TabsTitleControl extends TitleControl { // Run action on Enter/Space if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { handled = true; - const input = this.group.getEditor(index); + const input = this.group.getEditorByIndex(index); if (input) { this.group.openEditor(input); } @@ -581,7 +581,7 @@ export class TabsTitleControl extends TitleControl { targetIndex = this.group.count - 1; } - const target = this.group.getEditor(targetIndex); + const target = this.group.getEditorByIndex(targetIndex); if (target) { handled = true; this.group.openEditor(target, { preserveFocus: true }); @@ -603,7 +603,7 @@ export class TabsTitleControl extends TitleControl { disposables.add(addDisposableListener(tab, EventType.DBLCLICK, (e: MouseEvent) => { EventHelper.stop(e); - const editor = this.group.getEditor(index); + const editor = this.group.getEditorByIndex(index); if (editor && this.group.isPinned(editor)) { this.accessor.arrangeGroups(GroupsArrangement.TOGGLE, this.group); } else { @@ -615,7 +615,7 @@ export class TabsTitleControl extends TitleControl { disposables.add(addDisposableListener(tab, EventType.CONTEXT_MENU, (e: Event) => { EventHelper.stop(e, true); - const input = this.group.getEditor(index); + const input = this.group.getEditorByIndex(index); if (input) { this.onContextMenu(input, e, tab); } @@ -623,7 +623,7 @@ export class TabsTitleControl extends TitleControl { // Drag support disposables.add(addDisposableListener(tab, EventType.DRAG_START, (e: DragEvent) => { - const editor = this.group.getEditor(index); + const editor = this.group.getEditorByIndex(index); if (!editor) { return; } @@ -669,7 +669,7 @@ export class TabsTitleControl extends TitleControl { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); if (Array.isArray(data)) { const localDraggedEditor = data[0].identifier; - if (localDraggedEditor.editor === this.group.getEditor(index) && localDraggedEditor.groupId === this.group.id) { + if (localDraggedEditor.editor === this.group.getEditorByIndex(index) && localDraggedEditor.groupId === this.group.id) { if (e.dataTransfer) { e.dataTransfer.dropEffect = 'none'; } @@ -739,7 +739,7 @@ export class TabsTitleControl extends TitleControl { private updateDropFeedback(element: HTMLElement, isDND: boolean, index?: number): void { const isTab = (typeof index === 'number'); - const editor = typeof index === 'number' ? this.group.getEditor(index) : undefined; + const editor = typeof index === 'number' ? this.group.getEditorByIndex(index) : undefined; const isActiveTab = isTab && !!editor && this.group.isActive(editor); // Background diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index cb3930fa43a..4dfe5dd897f 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -6,17 +6,17 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { distinct, deepClone, assign } from 'vs/base/common/objects'; -import { isObject, assertIsDefined } from 'vs/base/common/types'; +import { isObject, assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { Dimension } from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorInput, EditorOptions, IEditorMemento, ITextEditor } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorMemento, ITextEditor, SaveReason } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorViewState, IEditor } from 'vs/editor/common/editorCommon'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ITextFileService, SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { isDiffEditor, isCodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -248,6 +248,15 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { this.editorMemento.saveEditorState(this.group, resource, editorViewState); } + getViewState(): IEditorViewState | undefined { + const resource = this.input?.getResource(); + if (resource) { + return withNullAsUndefined(this.retrieveTextEditorViewState(resource)); + } + + return undefined; + } + protected retrieveTextEditorViewState(resource: URI): IEditorViewState | null { const control = this.getControl(); if (!isCodeEditor(control)) { diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 82e2097540f..bf09dc1b0a6 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -127,7 +127,7 @@ cursor: pointer; min-width: 110px; min-height: 18px; - padding: 2px 8px; + padding: 2px 23px 2px 8px; } /* Rotate icons when panel is on right */ diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 4492478746c..dfc85945a82 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -33,7 +33,7 @@ */ transform: translate3d(0px, 0px, 0px); position: relative; - z-index: 1; + z-index: 1000; /* move the entire titlebar above the workbench, except modals/dialogs */ } .monaco-workbench .part.titlebar > .titlebar-drag-region { @@ -43,10 +43,14 @@ position: absolute; width: 100%; height: 100%; - z-index: -1; -webkit-app-region: drag; } +.monaco-workbench .part.titlebar > .menubar { + /* Move above drag region since negative z-index on that element causes AA issues */ + z-index: 1; +} + .monaco-workbench .part.titlebar > .window-title { flex: 0 1 auto; font-size: 12px; @@ -97,7 +101,7 @@ width: 35px; height: 100%; position: relative; - z-index: 3000; + z-index: 2; /* highest level of titlebar */ background-image: url('code-icon.svg'); background-repeat: no-repeat; background-position: center center; @@ -115,7 +119,7 @@ flex-shrink: 0; text-align: center; position: relative; - z-index: 3000; + z-index: 2; /* highest level of titlebar */ -webkit-app-region: no-drag; height: 100%; width: 138px; diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 0c3d435f0aa..4372732ae90 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -12,7 +12,7 @@ import { IAction, Action } from 'vs/base/common/actions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import * as DOM from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { isMacintosh, isWeb } from 'vs/base/common/platform'; +import { isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -674,7 +674,7 @@ export class CustomMenubarControl extends MenubarControl { } this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, () => { - if (this.menubar && !BrowserFeatures.pointerEvents) { + if (this.menubar && !(isIOS && BrowserFeatures.pointerEvents)) { this.menubar.blur(); } })); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 0bbd42aae20..2af28e59378 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -176,7 +176,7 @@ export class TitlebarPart extends Part implements ITitleService { } private onMenubarFocusChanged(focused: boolean) { - if (!isWeb && (isWindows || isLinux) && this.currentMenubarVisibility === 'compact' && this.dragRegion) { + if (!isWeb && (isWindows || isLinux) && this.currentMenubarVisibility !== 'compact' && this.dragRegion) { if (focused) { hide(this.dragRegion); } else { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index d2d5207bdec..807ac56d8f0 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -26,6 +26,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { onUnexpectedError } from 'vs/base/common/errors'; import * as browser from 'vs/base/browser/browser'; +import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; @@ -90,7 +91,10 @@ class BrowserMain extends Disposable { private registerListeners(workbench: Workbench, storageService: BrowserStorageService): void { // Layout - this._register(addDisposableListener(window, EventType.RESIZE, () => workbench.layout())); + const viewport = platform.isIOS && (window).visualViewport ? (window).visualViewport /** Visual viewport */ : window /** Layout viewport */; + this._register(addDisposableListener(viewport, EventType.RESIZE, () => { + workbench.layout(); + })); // Prevent the back/forward gestures in macOS this._register(addDisposableListener(this.domElement, EventType.WHEEL, (e) => { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 1764e7ed832..f85d5c0cfde 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -14,13 +14,17 @@ import { IInstantiationService, IConstructorSignature0, ServicesAccessor, Brande import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITextModel } from 'vs/editor/common/model'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ICompositeControl } from 'vs/workbench/common/composite'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { IPathData } from 'vs/platform/windows/common/windows'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; +import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { isEqual } from 'vs/base/common/resources'; +export const DirtyWorkingCopiesContext = new RawContextKey('dirtyWorkingCopies', false); export const ActiveEditorContext = new RawContextKey('activeEditor', null); export const ActiveEditorIsSaveableContext = new RawContextKey('activeEditorIsSaveable', false); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); @@ -119,6 +123,17 @@ export interface ITextEditor extends IEditor { * Returns the underlying text editor widget of this editor. */ getControl(): ICodeEditor | undefined; + + /** + * Returns the current view state of the text editor if any. + */ + getViewState(): IEditorViewState | undefined; +} + +export function isTextEditor(thing: IEditor | undefined): thing is ITextEditor { + const candidate = thing as ITextEditor | undefined; + + return typeof candidate?.getViewState === 'function'; } export interface ITextDiffEditor extends IEditor { @@ -261,15 +276,64 @@ export const enum Verbosity { LONG } -export interface IRevertOptions { +export const enum SaveReason { /** - * Forces to load the contents of the editor again even if the editor is not dirty. + * Explicit user gesture. + */ + EXPLICIT = 1, + + /** + * Auto save after a timeout. + */ + AUTO = 2, + + /** + * Auto save after editor focus change. + */ + FOCUS_CHANGE = 3, + + /** + * Auto save after window change. + */ + WINDOW_CHANGE = 4 +} + +export interface ISaveOptions { + + /** + * An indicator how the save operation was triggered. + */ + reason?: SaveReason; + + /** + * Forces to load the contents of the working copy + * again even if the working copy is not dirty. */ force?: boolean; /** - * A soft revert will clear dirty state of an editor but will not attempt to load it. + * Instructs the save operation to skip any save participants. + */ + skipSaveParticipants?: boolean; + + /** + * A hint as to which file systems should be available for saving. + */ + availableFileSystems?: string[]; +} + +export interface IRevertOptions { + + /** + * Forces to load the contents of the working copy + * again even if the working copy is not dirty. + */ + force?: boolean; + + /** + * A soft revert will clear dirty state of a working copy + * but will not attempt to load it from its persisted state. */ soft?: boolean; } @@ -279,7 +343,7 @@ export interface IEditorInput extends IDisposable { /** * Triggered when this input is disposed. */ - onDispose: Event; + readonly onDispose: Event; /** * Returns the associated resource of this input. @@ -311,11 +375,35 @@ export interface IEditorInput extends IDisposable { */ resolve(): Promise; + /** + * Returns if this input is readonly or not. + */ + isReadonly(): boolean; + + /** + * Returns if the input is an untitled editor or not. + */ + isUntitled(): boolean; + /** * Returns if this input is dirty or not. */ isDirty(): boolean; + /** + * Saves the editor. The provided groupId helps + * implementors to e.g. preserve view state of the editor + * and re-open it in the correct group after saving. + */ + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise; + + /** + * Saves the editor to a different location. The provided groupId + * helps implementors to e.g. preserve view state of the editor + * and re-open it in the correct group after saving. + */ + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise; + /** * Reverts this input. */ @@ -325,6 +413,11 @@ export interface IEditorInput extends IDisposable { * Returns if the other object matches this input. */ matches(other: unknown): boolean; + + /** + * Returns if this editor is disposed. + */ + isDisposed(): boolean; } /** @@ -333,14 +426,14 @@ export interface IEditorInput extends IDisposable { */ export abstract class EditorInput extends Disposable implements IEditorInput { - protected readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); - readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; + protected readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; - protected readonly _onDidChangeLabel: Emitter = this._register(new Emitter()); - readonly onDidChangeLabel: Event = this._onDidChangeLabel.event; + protected readonly _onDidChangeLabel = this._register(new Emitter()); + readonly onDidChangeLabel = this._onDidChangeLabel.event; - private readonly _onDispose: Emitter = this._register(new Emitter()); - readonly onDispose: Event = this._onDispose.event; + private readonly _onDispose = this._register(new Emitter()); + readonly onDispose = this._onDispose.event; private disposed: boolean = false; @@ -408,6 +501,22 @@ export abstract class EditorInput extends Disposable implements IEditorInput { */ abstract resolve(): Promise; + /** + * Returns if this input is readonly or not. + */ + isReadonly(): boolean { + // Subclasses need to explicitly opt-in to being editable. + return !this.isDirty(); + } + + /** + * Returns if the input is an untitled editor or not. + */ + isUntitled(): boolean { + // Subclasses need to explicitly opt-in to being untitled. + return false; + } + /** * An editor that is dirty will be asked to be saved once it closes. */ @@ -418,7 +527,14 @@ export abstract class EditorInput extends Disposable implements IEditorInput { /** * Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. */ - save(): Promise { + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return Promise.resolve(true); + } + + /** + * Saves the editor to a different location. + */ + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { return Promise.resolve(true); } @@ -429,13 +545,6 @@ export abstract class EditorInput extends Disposable implements IEditorInput { return Promise.resolve(true); } - /** - * Called when this input is no longer opened in any editor. Subclasses can free resources as needed. - */ - close(): void { - this.dispose(); - } - /** * Subclasses can set this to false if it does not make sense to split the editor input. */ @@ -469,6 +578,59 @@ export abstract class EditorInput extends Disposable implements IEditorInput { } } +export abstract class TextEditorInput extends EditorInput { + + constructor( + protected readonly resource: URI, + @IEditorService protected readonly editorService: IEditorService, + @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, + @ITextFileService protected readonly textFileService: ITextFileService + ) { + super(); + } + + getResource(): URI { + return this.resource; + } + + save(groupId: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.textFileService.save(this.resource, options); + } + + saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.doSaveAs(group, () => this.textFileService.saveAs(this.resource, undefined, options)); + } + + protected async doSaveAs(group: GroupIdentifier, saveRunnable: () => Promise, replaceAllEditors?: boolean): Promise { + + // Preserve view state by opening the editor first. In addition + // this allows the user to review the contents of the editor. + let viewState: IEditorViewState | undefined = undefined; + const editor = await this.editorService.openEditor(this, undefined, group); + if (isTextEditor(editor)) { + viewState = editor.getViewState(); + } + + // Save as + const target = await saveRunnable(); + if (!target) { + return false; // save cancelled + } + + // Replace editor preserving viewstate (either across all groups or + // only selected group) if the target is different from the current resource + if (!isEqual(target, this.resource)) { + const replacement = this.editorService.createInput({ resource: target }); + const targetGroups = replaceAllEditors ? this.editorGroupService.groups.map(group => group.id) : [group]; + for (const group of targetGroups) { + await this.editorService.replaceEditors([{ editor: this, replacement, options: { pinned: true, viewState } }], group); + } + } + + return true; + } +} + export const enum EncodingMode { /** @@ -556,16 +718,28 @@ export class SideBySideEditorInput extends EditorInput { return this._details; } + isReadonly(): boolean { + return this.master.isReadonly(); + } + + isUntitled(): boolean { + return this.master.isUntitled(); + } + isDirty(): boolean { return this.master.isDirty(); } - save(): Promise { - return this.master.save(); + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return this.master.save(groupId, options); } - revert(): Promise { - return this.master.revert(); + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return this.master.saveAs(groupId, options); + } + + revert(options?: IRevertOptions): Promise { + return this.master.revert(options); } getTelemetryDescriptor(): { [key: string]: unknown } { @@ -640,8 +814,8 @@ export interface ITextEditorModel extends IEditorModel { */ export class EditorModel extends Disposable implements IEditorModel { - private readonly _onDispose: Emitter = this._register(new Emitter()); - readonly onDispose: Event = this._onDispose.event; + private readonly _onDispose = this._register(new Emitter()); + readonly onDispose = this._onDispose.event; /** * Causes this model to load returning a promise when loading is completed. diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 72f1223b558..c8de4f91317 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -4,13 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { Extensions, IEditorInputFactoryRegistry, EditorInput, toResource, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorInput, SideBySideEditor } from 'vs/workbench/common/editor'; -import { URI } from 'vs/base/common/uri'; +import { Extensions, IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, CloseDirection, IEditorInput, SideBySideEditorInput } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ResourceMap } from 'vs/base/common/map'; import { coalesce } from 'vs/base/common/arrays'; const EditorOpenPositioning = { @@ -60,31 +58,31 @@ export class EditorGroup extends Disposable { //#region events private readonly _onDidEditorActivate = this._register(new Emitter()); - readonly onDidEditorActivate: Event = this._onDidEditorActivate.event; + readonly onDidEditorActivate = this._onDidEditorActivate.event; private readonly _onDidEditorOpen = this._register(new Emitter()); - readonly onDidEditorOpen: Event = this._onDidEditorOpen.event; + readonly onDidEditorOpen = this._onDidEditorOpen.event; private readonly _onDidEditorClose = this._register(new Emitter()); - readonly onDidEditorClose: Event = this._onDidEditorClose.event; + readonly onDidEditorClose = this._onDidEditorClose.event; private readonly _onDidEditorDispose = this._register(new Emitter()); - readonly onDidEditorDispose: Event = this._onDidEditorDispose.event; + readonly onDidEditorDispose = this._onDidEditorDispose.event; private readonly _onDidEditorBecomeDirty = this._register(new Emitter()); - readonly onDidEditorBecomeDirty: Event = this._onDidEditorBecomeDirty.event; + readonly onDidEditorBecomeDirty = this._onDidEditorBecomeDirty.event; private readonly _onDidEditorLabelChange = this._register(new Emitter()); - readonly onDidEditorLabelChange: Event = this._onDidEditorLabelChange.event; + readonly onDidEditorLabelChange = this._onDidEditorLabelChange.event; private readonly _onDidEditorMove = this._register(new Emitter()); - readonly onDidEditorMove: Event = this._onDidEditorMove.event; + readonly onDidEditorMove = this._onDidEditorMove.event; private readonly _onDidEditorPin = this._register(new Emitter()); - readonly onDidEditorPin: Event = this._onDidEditorPin.event; + readonly onDidEditorPin = this._onDidEditorPin.event; private readonly _onDidEditorUnpin = this._register(new Emitter()); - readonly onDidEditorUnpin: Event = this._onDidEditorUnpin.event; + readonly onDidEditorUnpin = this._onDidEditorUnpin.event; //#endregion @@ -93,7 +91,6 @@ export class EditorGroup extends Disposable { private editors: EditorInput[] = []; private mru: EditorInput[] = []; - private mapResourceToEditorCount: ResourceMap = new ResourceMap(); private preview: EditorInput | null = null; // editor in preview state private active: EditorInput | null = null; // editor in active state @@ -135,26 +132,8 @@ export class EditorGroup extends Disposable { return mru ? this.mru.slice(0) : this.editors.slice(0); } - getEditor(index: number): EditorInput | undefined; - getEditor(resource: URI): EditorInput | undefined; - getEditor(arg1: number | URI): EditorInput | undefined { - if (typeof arg1 === 'number') { - return this.editors[arg1]; - } - - const resource: URI = arg1; - if (!this.contains(resource)) { - return undefined; // fast check for resource opened or not - } - - for (const editor of this.editors) { - const editorResource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); - if (editorResource?.toString() === resource.toString()) { - return editor; - } - } - - return undefined; + getEditorByIndex(index: number): EditorInput | undefined { + return this.editors[index]; } get activeEditor(): EditorInput | null { @@ -509,7 +488,6 @@ export class EditorGroup extends Disposable { // Add if (!del && editor) { this.mru.push(editor); // make it LRU editor - this.updateResourceMap(editor, false /* add */); // add new to resource map } // Remove / Replace @@ -519,41 +497,11 @@ export class EditorGroup extends Disposable { // Remove if (del && !editor) { this.mru.splice(indexInMRU, 1); // remove from MRU - this.updateResourceMap(editorToDeleteOrReplace, true /* delete */); // remove from resource map } // Replace else if (del && editor) { this.mru.splice(indexInMRU, 1, editor); // replace MRU at location - this.updateResourceMap(editor, false /* add */); // add new to resource map - this.updateResourceMap(editorToDeleteOrReplace, true /* delete */); // remove replaced from resource map - } - } - } - - private updateResourceMap(editor: EditorInput, remove: boolean): void { - const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); - if (resource) { - - // It is possible to have the same resource opened twice (once as normal input and once as diff input) - // So we need to do ref counting on the resource to provide the correct picture - const counter = this.mapResourceToEditorCount.get(resource) || 0; - - // Add - let newCounter: number; - if (!remove) { - newCounter = counter + 1; - } - - // Delete - else { - newCounter = counter - 1; - } - - if (newCounter > 0) { - this.mapResourceToEditorCount.set(resource, newCounter); - } else { - this.mapResourceToEditorCount.delete(resource); } } } @@ -572,28 +520,20 @@ export class EditorGroup extends Disposable { return -1; } - contains(editorOrResource: EditorInput | URI): boolean; - contains(editor: EditorInput, supportSideBySide?: boolean): boolean; - contains(editorOrResource: EditorInput | URI, supportSideBySide?: boolean): boolean { - if (editorOrResource instanceof EditorInput) { - const index = this.indexOf(editorOrResource); - if (index >= 0) { + contains(candidate: EditorInput, searchInSideBySideEditors?: boolean): boolean { + for (const editor of this.editors) { + if (this.matches(editor, candidate)) { return true; } - if (supportSideBySide && editorOrResource instanceof SideBySideEditorInput) { - const index = this.indexOf(editorOrResource.master); - if (index >= 0) { + if (searchInSideBySideEditors && editor instanceof SideBySideEditorInput) { + if (this.matches(editor.master, candidate) || this.matches(editor.details, candidate)) { return true; } } - - return false; } - const counter = this.mapResourceToEditorCount.get(editorOrResource); - - return typeof counter === 'number' && counter > 0; + return false; } private setMostRecentlyUsed(editor: EditorInput): void { @@ -619,7 +559,6 @@ export class EditorGroup extends Disposable { const group = this.instantiationService.createInstance(EditorGroup, undefined); group.editors = this.editors.slice(0); group.mru = this.mru.slice(0); - group.mapResourceToEditorCount = this.mapResourceToEditorCount.clone(); group.preview = this.preview; group.active = this.active; group.editorOpenPositioning = this.editorOpenPositioning; @@ -678,7 +617,6 @@ export class EditorGroup extends Disposable { const editor = factory.deserialize(this.instantiationService, e.value); if (editor) { this.registerEditorListeners(editor); - this.updateResourceMap(editor, false /* add */); } return editor; diff --git a/src/vs/workbench/common/editor/untitledTextEditorInput.ts b/src/vs/workbench/common/editor/untitledTextEditorInput.ts index 3e2998c3e29..8be428a4931 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorInput.ts @@ -7,43 +7,55 @@ import { URI } from 'vs/base/common/uri'; import { suggestFilename } from 'vs/base/common/mime'; import { createMemoizer } from 'vs/base/common/decorators'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; -import { EditorInput, IEncodingSupport, EncodingMode, Verbosity, IModeSupport } from 'vs/workbench/common/editor'; +import { basenameOrAuthority, dirname, toLocalResource } from 'vs/base/common/resources'; +import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport, TextEditorInput, GroupIdentifier, IRevertOptions } from 'vs/workbench/common/editor'; import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { Emitter } from 'vs/base/common/event'; +import { ITextFileService, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ILabelService } from 'vs/platform/label/common/label'; import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; /** * An editor input to be used for untitled text buffers. */ -export class UntitledTextEditorInput extends EditorInput implements IEncodingSupport, IModeSupport { +export class UntitledTextEditorInput extends TextEditorInput implements IEncodingSupport, IModeSupport { static readonly ID: string = 'workbench.editors.untitledEditorInput'; + private static readonly MEMOIZER = createMemoizer(); private cachedModel: UntitledTextEditorModel | null = null; private modelResolve: Promise | null = null; - private readonly _onDidModelChangeContent: Emitter = this._register(new Emitter()); - readonly onDidModelChangeContent: Event = this._onDidModelChangeContent.event; + private readonly _onDidModelChangeContent = this._register(new Emitter()); + readonly onDidModelChangeContent = this._onDidModelChangeContent.event; - private readonly _onDidModelChangeEncoding: Emitter = this._register(new Emitter()); - readonly onDidModelChangeEncoding: Event = this._onDidModelChangeEncoding.event; + private readonly _onDidModelChangeEncoding = this._register(new Emitter()); + readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event; constructor( - private readonly resource: URI, + resource: URI, private readonly _hasAssociatedFilePath: boolean, private preferredMode: string | undefined, private readonly initialValue: string | undefined, private preferredEncoding: string | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextFileService private readonly textFileService: ITextFileService, - @ILabelService private readonly labelService: ILabelService + @ITextFileService textFileService: ITextFileService, + @ILabelService private readonly labelService: ILabelService, + @IEditorService editorService: IEditorService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { - super(); + super(resource, editorService, editorGroupService, textFileService); + + this.registerListeners(); + } + + private registerListeners(): void { this._register(this.labelService.onDidChangeFormatters(() => UntitledTextEditorInput.MEMOIZER.clear())); } @@ -55,10 +67,6 @@ export class UntitledTextEditorInput extends EditorInput implements IEncodingSup return UntitledTextEditorInput.ID; } - getResource(): URI { - return this.resource; - } - getName(): string { return this.hasAssociatedFilePath ? basenameOrAuthority(this.resource) : this.resource.path; } @@ -124,6 +132,14 @@ export class UntitledTextEditorInput extends EditorInput implements IEncodingSup } } + isReadonly(): boolean { + return false; + } + + isUntitled(): boolean { + return true; + } + isDirty(): boolean { if (this.cachedModel) { return this.cachedModel.isDirty(); @@ -146,11 +162,30 @@ export class UntitledTextEditorInput extends EditorInput implements IEncodingSup return false; } - save(): Promise { - return this.textFileService.save(this.resource); + save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.doSaveAs(group, async () => { + + // With associated file path, save to the path that is + // associated. Make sure to convert the result using + // remote authority properly. + if (this.hasAssociatedFilePath) { + if (await this.textFileService.save(this.resource, options)) { + return toLocalResource(this.resource, this.environmentService.configuration.remoteAuthority); + } + + return; + } + + // Without associated file path, do a normal "Save As" + return this.textFileService.saveAs(this.resource, undefined, options); + }, true /* replace editor across all groups */); } - revert(): Promise { + saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.doSaveAs(group, () => this.textFileService.saveAs(this.resource, undefined, options), true /* replace editor across all groups */); + } + + revert(options?: IRevertOptions): Promise { if (this.cachedModel) { this.cachedModel.revert(); } diff --git a/src/vs/workbench/common/editor/untitledTextEditorModel.ts b/src/vs/workbench/common/editor/untitledTextEditorModel.ts index b0d22deb9b3..ccb30868d92 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEncodingSupport } from 'vs/workbench/common/editor'; +import { IEncodingSupport, ISaveOptions } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { URI } from 'vs/base/common/uri'; import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files'; @@ -17,6 +17,7 @@ import { ITextBufferFactory } from 'vs/editor/common/model'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export class UntitledTextEditorModel extends BaseTextEditorModel implements IEncodingSupport, IWorkingCopy { @@ -48,7 +49,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc @IModelService modelService: IModelService, @IBackupFileService private readonly backupFileService: IBackupFileService, @ITextResourceConfigurationService private readonly configurationService: ITextResourceConfigurationService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @ITextFileService private readonly textFileService: ITextFileService ) { super(modelService, modeService); @@ -115,11 +117,17 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc this._onDidChangeDirty.fire(); } - revert(): void { + save(options?: ISaveOptions): Promise { + return this.textFileService.save(this.resource, options); + } + + async revert(): Promise { this.setDirty(false); // Handle content change event buffered this.contentChangeEventScheduler.schedule(); + + return true; } backup(): Promise { diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 81c0439428b..e465329f6b0 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -352,6 +352,12 @@ export const ACTIVITY_BAR_ACTIVE_BORDER = registerColor('activityBar.activeBorde hc: null }, nls.localize('activityBarActiveBorder', "Activity bar border color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_ACTIVE_FOCUS_BORDER = registerColor('activityBar.activeFocusBorder', { + dark: null, + light: null, + hc: null +}, nls.localize('activityBarActiveFocusBorder', "Activity bar focus border color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); + export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeBackground', { dark: null, light: null, diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 223949c9ac6..b7f1355fdad 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -82,7 +82,7 @@ export interface ISuggestEnabledInputStyleOverrides extends IStyleOverrides { } type ISuggestEnabledInputStyles = { - [P in keyof ISuggestEnabledInputStyleOverrides]: Color; + [P in keyof ISuggestEnabledInputStyleOverrides]: Color | undefined; }; export function attachSuggestEnabledInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: ISuggestEnabledInputStyleOverrides): IDisposable { diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 299a7271b2b..c499a52051e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -8,7 +8,6 @@ import * as nls from 'vs/nls'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel'; @@ -127,12 +126,7 @@ export class CommentNodeRenderer implements IListRenderer inline: true, actionHandler: { callback: (content) => { - try { - const uri = URI.parse(content); - this.openerService.open(uri).catch(onUnexpectedError); - } catch (err) { - // ignore - } + this.openerService.open(content).catch(onUnexpectedError); }, disposeables: disposables } diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 996aa3ceb7e..cf1118961f2 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -4,15 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { firstOrDefault } from 'vs/base/common/arrays'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { URI } from 'vs/base/common/uri'; +import { Command } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import * as nls from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService } from 'vs/platform/list/browser/listService'; import { IEditorCommandsContext } from 'vs/workbench/common/editor'; -import { ICustomEditorService, CONTEXT_HAS_CUSTOM_EDITORS } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -98,3 +102,72 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { }); // #endregion + + +(new class UndoCustomEditorCommand extends Command { + public static readonly ID = 'editor.action.customEditor.undo'; + + constructor() { + super({ + id: UndoCustomEditorCommand.ID, + precondition: ContextKeyExpr.and( + CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, + ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, + weight: KeybindingWeight.EditorContrib + } + }); + } + + public runCommand(accessor: ServicesAccessor): void { + const customEditorService = accessor.get(ICustomEditorService); + + const activeCustomEditor = customEditorService.activeCustomEditor; + if (!activeCustomEditor) { + return; + } + + const model = customEditorService.models.get(activeCustomEditor.resource, activeCustomEditor.viewType); + if (!model) { + return; + } + + model.undo(); + } +}).register(); + +(new class RedoWebviewEditorCommand extends Command { + public static readonly ID = 'editor.action.customEditor.redo'; + + constructor() { + super({ + id: RedoWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and( + CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, + ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z }, + weight: KeybindingWeight.EditorContrib + } + }); + } + + public runCommand(accessor: ServicesAccessor): void { + const customEditorService = accessor.get(ICustomEditorService); + + const activeCustomEditor = customEditorService.activeCustomEditor; + if (!activeCustomEditor) { + return; + } + + const model = customEditorService.models.get(activeCustomEditor.resource, activeCustomEditor.viewType); + if (!model) { + return; + } + + model.redo(); + } +}).register(); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index d89fcb9b26f..9a2e4342f70 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -12,10 +12,10 @@ import { DataUri, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IEditorInput, Verbosity } from 'vs/workbench/common/editor'; +import { IEditorInput, Verbosity, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; -import { CustomEditorModel } from './customEditorModel'; +import { CustomEditorModel } from '../common/customEditorModel'; export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @@ -116,7 +116,24 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); } + public isReadonly(): boolean { + return false; + } + public isDirty(): boolean { return this._model ? this._model.isDirty() : false; } + + public save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return this._model ? this._model.save(options) : Promise.resolve(false); + } + + public saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + // TODO@matt implement properly (see TextEditorInput#saveAs()) + return this._model ? this._model.save(options) : Promise.resolve(false); + } + + public revert(options?: IRevertOptions): Promise { + return this._model ? this._model.revert(options) : Promise.resolve(false); + } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 5a0b6a47e3b..56c6b0320a9 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -3,15 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, distinct, mergeSort, find } from 'vs/base/common/arrays'; +import { coalesce, distinct, find, mergeSort } from 'vs/base/common/arrays'; import * as glob from 'vs/base/common/glob'; -import { UnownedDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable, UnownedDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename, DataUri, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -21,14 +23,14 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { EditorInput, EditorOptions, IEditor, IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; -import { CustomEditorPriority, CustomEditorInfo, CustomEditorSelector, ICustomEditorService, CONTEXT_HAS_CUSTOM_EDITORS } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, CustomEditorInfo, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { CustomFileEditorInput } from './customEditorInput'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { Lazy } from 'vs/base/common/lazy'; const defaultEditorId = 'default'; @@ -41,7 +43,7 @@ const defaultEditorInfo: CustomEditorInfo = { priority: CustomEditorPriority.default, }; -export class CustomEditorStore { +export class CustomEditorInfoStore { private readonly contributedEditors = new Map(); public clear() { @@ -71,11 +73,17 @@ export class CustomEditorStore { export class CustomEditorService extends Disposable implements ICustomEditorService { _serviceBrand: any; - private readonly editors = new CustomEditorStore(); + private readonly _editorInfoStore = new CustomEditorInfoStore(); + + private readonly _models: CustomEditorModelManager; + private readonly _hasCustomEditor: IContextKey; + private readonly _focusedCustomEditorIsEditable: IContextKey; + private readonly _webviewHasOwnEditFunctions: IContextKey; constructor( @IContextKeyService contextKeyService: IContextKeyService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -84,12 +92,14 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ) { super(); + this._models = new CustomEditorModelManager(workingCopyService); + webviewEditorsExtensionPoint.setHandler(extensions => { - this.editors.clear(); + this._editorInfoStore.clear(); for (const extension of extensions) { for (const webviewEditorContribution of extension.value) { - this.editors.add({ + this._editorInfoStore.add({ id: webviewEditorContribution.viewType, displayName: webviewEditorContribution.displayName, selector: webviewEditorContribution.selector || [], @@ -97,24 +107,37 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ }); } } - this.updateContext(); + this.updateContexts(); }); this._hasCustomEditor = CONTEXT_HAS_CUSTOM_EDITORS.bindTo(contextKeyService); + this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); + this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); - this._register(this.editorService.onDidActiveEditorChange(() => this.updateContext())); - this.updateContext(); + this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); + this.updateContexts(); + } + + public get models() { return this._models; } + + public get activeCustomEditor(): ICustomEditor | undefined { + const activeInput = this.editorService.activeControl?.input; + if (!(activeInput instanceof CustomFileEditorInput)) { + return undefined; + } + const resource = activeInput.getResource(); + return { resource, viewType: activeInput.viewType }; } public getContributedCustomEditors(resource: URI): readonly CustomEditorInfo[] { - return this.editors.getContributedEditors(resource); + return this._editorInfoStore.getContributedEditors(resource); } public getUserConfiguredCustomEditors(resource: URI): readonly CustomEditorInfo[] { const rawAssociations = this.configurationService.getValue(customEditorsAssociationsKey) || []; return coalesce(rawAssociations .filter(association => matches(association, resource)) - .map(association => this.editors.get(association.viewType))); + .map(association => this._editorInfoStore.get(association.viewType))); } public async promptOpenWith( @@ -164,7 +187,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this.openEditorForResource(resource, fileInput, { ...options, ignoreOverrides: true }, group); } - if (!this.editors.get(viewType)) { + if (!this._editorInfoStore.get(viewType)) { return this.promptOpenWith(resource, options, group); } @@ -215,22 +238,23 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this.editorService.openEditor(input, options, group); } - private updateContext() { + private updateContexts() { const activeControl = this.editorService.activeControl; - if (!activeControl) { - this._hasCustomEditor.reset(); - return; - } - const resource = activeControl.input.getResource(); + const resource = activeControl?.input.getResource(); if (!resource) { this._hasCustomEditor.reset(); + this._focusedCustomEditorIsEditable.reset(); + this._webviewHasOwnEditFunctions.reset(); return; } + const possibleEditors = [ ...this.getContributedCustomEditors(resource), ...this.getUserConfiguredCustomEditors(resource), ]; this._hasCustomEditor.set(possibleEditors.length > 0); + this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomFileEditorInput); + this._webviewHasOwnEditFunctions.set(true); } } diff --git a/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts b/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts index c5aa8b431bb..cfd19134b04 100644 --- a/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts +++ b/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts @@ -10,6 +10,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { CustomEditoInputFactory } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; @@ -18,7 +19,6 @@ import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEdito import './commands'; import { CustomFileEditorInput } from './customEditorInput'; import { CustomEditorContribution, customEditorsAssociationsKey, CustomEditorService } from './customEditors'; -import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; registerSingleton(ICustomEditorService, CustomEditorService); diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index a0bda2e0764..b0f65e94a60 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -3,20 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { EditorInput, IEditor } from 'vs/workbench/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const ICustomEditorService = createDecorator('customEditorService'); export const CONTEXT_HAS_CUSTOM_EDITORS = new RawContextKey('hasCustomEditors', false); +export const CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE = new RawContextKey('focusedCustomEditorIsEditable', false); + +export interface ICustomEditor { + readonly resource: URI; + readonly viewType: string; +} export interface ICustomEditorService { _serviceBrand: any; + readonly models: ICustomEditorModelManager; + + readonly activeCustomEditor: ICustomEditor | undefined; + getContributedCustomEditors(resource: URI): readonly CustomEditorInfo[]; getUserConfiguredCustomEditors(resource: URI): readonly CustomEditorInfo[]; @@ -26,6 +38,26 @@ export interface ICustomEditorService { promptOpenWith(resource: URI, options?: ITextEditorOptions, group?: IEditorGroup): Promise; } +export type CustomEditorEdit = string; + +export interface ICustomEditorModelManager { + get(resource: URI, viewType: string): ICustomEditorModel | undefined; + + loadOrCreate(resource: URI, viewType: string): Promise; + + disposeModel(model: ICustomEditorModel): void; +} + +export interface ICustomEditorModel extends IWorkingCopy { + readonly onUndo: Event; + readonly onRedo: Event; + + undo(): void; + redo(): void; + + makeEdit(data: string): void; +} + export const enum CustomEditorPriority { default = 'default', builtin = 'builtin', diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts similarity index 50% rename from src/vs/workbench/contrib/customEditor/browser/customEditorModel.ts rename to src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index 4afe96cdc35..2d1988748da 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -6,22 +6,20 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { IWorkingCopy, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ICustomEditorModel, CustomEditorEdit } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; -type Edit = string; +export class CustomEditorModel extends Disposable implements ICustomEditorModel { -export class CustomEditorModel extends Disposable implements IWorkingCopy { - - private _currentEditIndex: number = 0; + private _currentEditIndex: number = -1; private _savePoint: number = -1; - private _edits: Array = []; + private _edits: Array = []; constructor( private readonly _resource: URI, - @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { super(); - this._register(this._workingCopyService.registerWorkingCopy(this)); } //#region IWorkingCopy @@ -43,8 +41,11 @@ export class CustomEditorModel extends Disposable implements IWorkingCopy { //#endregion - protected readonly _onUndo: Emitter = this._register(new Emitter()); - readonly onUndo: Event = this._onUndo.event; + protected readonly _onUndo = this._register(new Emitter()); + readonly onUndo: Event = this._onUndo.event; + + protected readonly _onRedo = this._register(new Emitter()); + readonly onRedo: Event = this._onRedo.event; public makeEdit(data: string): void { this._edits.splice(this._currentEditIndex, this._edits.length - this._currentEditIndex, data); @@ -56,17 +57,46 @@ export class CustomEditorModel extends Disposable implements IWorkingCopy { this._onDidChangeDirty.fire(); } - public save() { + public async save(options?: ISaveOptions) { this._savePoint = this._edits.length; this.updateDirty(); + + return true; + } + + public async revert(options?: IRevertOptions) { + while (this._currentEditIndex > 0) { + this.undo(); + } + + return true; } public undo() { - if (this._currentEditIndex >= 0) { - const undoneEdit = this._edits[this._currentEditIndex]; - --this._currentEditIndex; - this._onUndo.fire(undoneEdit); + if (this._currentEditIndex < 0) { + // nothing to undo + return; } + + const undoneEdit = this._edits[this._currentEditIndex]; + --this._currentEditIndex; + this._onUndo.fire(undoneEdit); + + this.updateDirty(); + } + + public redo() { + if (this._currentEditIndex >= this._edits.length - 1) { + // nothing to redo + return; + } + + ++this._currentEditIndex; + const redoneEdit = this._edits[this._currentEditIndex]; + this._onRedo.fire(redoneEdit); + this.updateDirty(); } } + + diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts new file mode 100644 index 00000000000..b97897df8c4 --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ICustomEditorModel, ICustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditorModel'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; + +export class CustomEditorModelManager implements ICustomEditorModelManager { + private readonly _models = new Map(); + + constructor( + @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, + ) { } + + + public get(resource: URI, viewType: string): ICustomEditorModel | undefined { + return this._models.get(this.key(resource, viewType))?.model; + } + + public async loadOrCreate(resource: URI, viewType: string): Promise { + const existing = this.get(resource, viewType); + if (existing) { + return existing; + } + + const model = new CustomEditorModel(resource); + const disposables = new DisposableStore(); + this._workingCopyService.registerWorkingCopy(model); + this._models.set(this.key(resource, viewType), { model, disposables }); + return model; + } + + public disposeModel(model: ICustomEditorModel): void { + let foundKey: string | undefined; + this._models.forEach((value, key) => { + if (model === value.model) { + value.disposables.dispose(); + foundKey = key; + } + }); + if (typeof foundKey === 'string') { + this._models.delete(foundKey); + } + return; + } + + private key(resource: URI, viewType: string): string { + return `${resource.toString()}@@@${viewType}`; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index f5e1e7b7a1b..736d91a7d0c 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -43,7 +43,7 @@ interface IBreakpointDecoration { } const breakpointHelperDecoration: IModelDecorationOptions = { - glyphMarginClassName: 'debug-breakpoint-hint', + glyphMarginClassName: 'codicon-debug-hint', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; @@ -91,7 +91,7 @@ function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoi } return { - glyphMarginClassName: className, + glyphMarginClassName: `${className}`, glyphMarginHoverMessage, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, beforeContentClassName: breakpoint.column ? `debug-breakpoint-placeholder` : undefined, @@ -239,7 +239,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { } this.ensureBreakpointHintDecoration(showBreakpointHintAtLineNumber); })); - this.toDispose.push(this.editor.onMouseLeave((e: IEditorMouseEvent) => { + this.toDispose.push(this.editor.onMouseLeave(() => { this.ensureBreakpointHintDecoration(-1); })); @@ -344,7 +344,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { const decorations = this.editor.getLineDecorations(line); if (decorations) { for (const { options } of decorations) { - if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf('debug') === -1) { + if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf('codicon-') === -1) { return false; } } @@ -422,7 +422,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { // Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there // In practice this happens for the first breakpoint that was set on a line // We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information - const cssClass = candidate.breakpoint ? getBreakpointMessageAndClassName(this.debugService, candidate.breakpoint).className : 'debug-breakpoint-disabled'; + const cssClass = candidate.breakpoint ? getBreakpointMessageAndClassName(this.debugService, candidate.breakpoint).className : 'codicon-debug-breakpoint-disabled'; const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn); const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, cssClass, candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions); @@ -543,6 +543,7 @@ class InlineBreakpointWidget implements IContentWidget, IDisposable { private create(cssClass: string | null | undefined): void { this.domNode = $('.inline-breakpoint-widget'); + this.domNode.classList.add('codicon'); if (cssClass) { this.domNode.classList.add(cssClass); } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index aad8c7e6944..a23fddadbb6 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -346,7 +346,7 @@ class BreakpointsRenderer implements IListRenderer 1) { + if (sessions.length > 1 || this.debugService.getViewModel().isMultiSessionView()) { return Promise.resolve(sessions.filter(s => !s.parentSession)); } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index a25124564ae..479de2952de 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -47,7 +47,6 @@ import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchEx import { VariablesView } from 'vs/workbench/contrib/debug/browser/variablesView'; import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; -import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; import { DebugCallStackContribution } from 'vs/workbench/contrib/debug/browser/debugCallStackContribution'; class OpenDebugViewletAction extends ShowViewletAction { @@ -278,7 +277,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Debug toolbar -const registerDebugToolBarItem = (id: string, title: string, iconLightUri: URI, iconDarkUri: URI, order: number, when?: ContextKeyExpr, precondition?: ContextKeyExpr) => { +const registerDebugToolBarItem = (id: string, title: string, order: number, iconClassName?: string, iconLightUri?: URI, iconDarkUri?: URI, when?: ContextKeyExpr, precondition?: ContextKeyExpr) => { MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { group: 'navigation', when, @@ -286,6 +285,7 @@ const registerDebugToolBarItem = (id: string, title: string, iconLightUri: URI, command: { id, title, + iconClassName, iconLocation: { light: iconLightUri, dark: iconDarkUri @@ -295,16 +295,16 @@ const registerDebugToolBarItem = (id: string, title: string, iconLightUri: URI, }); }; -registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/continue-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/continue-dark.svg')), 10, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/pause-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/pause-dark.svg')), 10, CONTEXT_DEBUG_STATE.notEqualsTo('stopped')); -registerDebugToolBarItem(STOP_ID, STOP_LABEL, URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/stop-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/stop-dark.svg')), 70, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); -registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/disconnect-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/disconnect-dark.svg')), 70, CONTEXT_FOCUSED_SESSION_IS_ATTACH); -registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/step-over-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/step-over-dark.svg')), 20, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/step-into-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/step-into-dark.svg')), 30, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/step-out-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/step-out-dark.svg')), 40, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/restart-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/restart-dark.svg')), 60); -registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/step-back-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/step-back-dark.svg')), 50, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); -registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/reverse-continue-light.svg')), URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/debug/browser/media/reverse-continue-dark.svg')), 60, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, 'codicon-debug-continue', undefined, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, 'codicon-debug-pause', undefined, undefined, CONTEXT_DEBUG_STATE.notEqualsTo('stopped')); +registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, 'codicon-debug-stop', undefined, undefined, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); +registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, 'codicon-debug-disconnect', undefined, undefined, CONTEXT_FOCUSED_SESSION_IS_ATTACH); +registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, 'codicon-debug-step-over', undefined, undefined, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, 'codicon-debug-step-into', undefined, undefined, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, 'codicon-debug-step-out', undefined, undefined, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, 'codicon-debug-restart', undefined, undefined); +registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back"), 50, 'codicon-debug-step-back', undefined, undefined, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, 'codicon-debug-continue', undefined, undefined, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); // Debug callstack context menu const registerDebugCallstackItem = (id: string, title: string, order: number, when?: ContextKeyExpr, precondition?: ContextKeyExpr, group = 'navigation') => { diff --git a/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts b/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts index d102042652b..ee78a18990e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts @@ -128,12 +128,12 @@ export class DebugCallStackContribution implements IWorkbenchContribution { static readonly STICKINESS = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; // we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. private static TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { - glyphMarginClassName: 'debug-top-stack-frame', + glyphMarginClassName: 'codicon-debug-breakpoint-stackframe', stickiness }; private static FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { - glyphMarginClassName: 'debug-focused-stack-frame', + glyphMarginClassName: 'codicon-debug-breakpoint-stackframe-focused', stickiness }; @@ -176,7 +176,68 @@ registerThemingParticipant((theme, collector) => { if (focusedStackFrame) { collector.addRule(`.monaco-editor .view-overlays .debug-focused-stack-frame-line { background: ${focusedStackFrame}; }`); } + + const debugIconBreakpointColor = theme.getColor(debugIconBreakpointForeground); + if (debugIconBreakpointColor) { + collector.addRule(` + .monaco-workbench .codicon-debug-breakpoint, + .monaco-workbench .codicon-debug-breakpoint-conditional, + .monaco-workbench .codicon-debug-breakpoint-log, + .monaco-workbench .codicon-debug-breakpoint-function, + .monaco-workbench .codicon-debug-breakpoint-data, + .monaco-workbench .codicon-debug-breakpoint-unsupported, + .monaco-workbench .codicon-debug-hint:not(*[class*='codicon-debug-breakpoint']) , + .monaco-workbench .codicon-debug-breakpoint-stackframe-dot, + .monaco-workbench .codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe-focused::after { + color: ${debugIconBreakpointColor} !important; + } + `); + } + + const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground); + if (debugIconBreakpointDisabledColor) { + collector.addRule(` + .monaco-workbench .codicon[class*='-disabled'] { + color: ${debugIconBreakpointDisabledColor} !important; + } + `); + } + + const debugIconBreakpointUnverifiedColor = theme.getColor(debugIconBreakpointUnverifiedForeground); + if (debugIconBreakpointUnverifiedColor) { + collector.addRule(` + .monaco-workbench .codicon[class*='-unverified'] { + color: ${debugIconBreakpointUnverifiedColor} !important; + } + `); + } + + const debugIconBreakpointStackframeColor = theme.getColor(debugIconBreakpointStackframeForeground); + if (debugIconBreakpointStackframeColor) { + collector.addRule(` + .monaco-workbench .codicon-debug-breakpoint-stackframe, + .monaco-workbench .codicon-debug-breakpoint-stackframe-dot::after { + color: ${debugIconBreakpointStackframeColor} !important; + } + `); + } + + const debugIconBreakpointStackframeFocusedColor = theme.getColor(debugIconBreakpointStackframeFocusedForeground); + if (debugIconBreakpointStackframeFocusedColor) { + collector.addRule(` + .monaco-workbench .codicon-debug-breakpoint-stackframe-focused { + color: ${debugIconBreakpointStackframeFocusedColor} !important; + } + `); + } + }); const topStackFrameColor = registerColor('editor.stackFrameHighlightBackground', { dark: '#ffff0033', light: '#ffff6673', hc: '#fff600' }, localize('topStackFrameLineHighlight', 'Background color for the highlight of line at the top stack frame position.')); const focusedStackFrameColor = registerColor('editor.focusedStackFrameHighlightBackground', { dark: '#7abd7a4d', light: '#cee7ce73', hc: '#cee7ce' }, localize('focusedStackFrameLineHighlight', 'Background color for the highlight of line at focused stack frame position.')); + +const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', { dark: '#E51400', light: '#E51400', hc: '#E51400' }, localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); +const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', { dark: '#848484', light: '#848484', hc: '#848484' }, localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); +const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', { dark: '#848484', light: '#848484', hc: '#848484' }, localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); +const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#FFCC00', light: '#FFCC00', hc: '#FFCC00' }, localize('debugIcon.breakpointStackframeForeground', 'Icon color for breakpoints.')); +const debugIconBreakpointStackframeFocusedForeground = registerColor('debugIcon.breakpointStackframeFocusedForeground', { dark: '#89D185', light: '#89D185', hc: '#89D185' }, localize('debugIcon.breakpointStackframeFocusedForeground', 'Icon color for breakpoints.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 28fad67b439..b68c4991ebb 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -12,7 +12,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardTokenType } from 'vs/editor/common/modes'; import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/model/wordHelper'; -import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -80,7 +80,7 @@ class DebugEditorContribution implements IDebugEditorContribution { this.toDispose.push(this.editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(e))); this.toDispose.push(this.editor.onMouseUp(() => this.mouseDown = false)); this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e))); - this.toDispose.push(this.editor.onMouseLeave((e: IEditorMouseEvent) => { + this.toDispose.push(this.editor.onMouseLeave((e: IPartialEditorMouseEvent) => { this.provideNonDebugHoverScheduler.cancel(); const hoverDomNode = this.hoverWidget.getDomNode(); if (!hoverDomNode) { diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 031da0c11ab..94c07e70c9c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -258,7 +258,7 @@ export class DebugService implements IDebugService { try { // make sure to save all files and that the configuration is up to date await this.extensionService.activateByEvent('onDebug'); - await this.textFileService.saveAll(); + await this.editorService.saveAll(); await this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined); await this.extensionService.whenInstalledExtensionsRegistered(); @@ -568,7 +568,7 @@ export class DebugService implements IDebugService { } async restartSession(session: IDebugSession, restartData?: any): Promise { - await this.textFileService.saveAll(); + await this.editorService.saveAll(); const isAutoRestart = !!restartData; const runTasks: () => Promise = async () => { diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 781ac16a5de..e939c92fd67 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -102,6 +102,12 @@ export const debugIconContinueForeground = registerColor('debugIcon.continueFore hc: '#75BEFF' }, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); +export const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' +}, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); + export class DebugToolBar extends Themable implements IWorkbenchContribution { private $el: HTMLElement; @@ -111,6 +117,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { private updateScheduler: RunOnceScheduler; private debugToolBarMenu: IMenu; private disposeOnUpdate: IDisposable | undefined; + private yCoordinate = 0; private isVisible = false; private isBuilt = false; @@ -264,9 +271,10 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } - private setYCoordinate(y = 0): void { + private setYCoordinate(y = this.yCoordinate): void { const titlebarOffset = this.layoutService.getTitleBarOffset(); this.$el.style.top = `${titlebarOffset + y}px`; + this.yCoordinate = y; } private setCoordinates(x?: number, y?: number): void { @@ -391,4 +399,9 @@ registerThemingParticipant((theme, collector) => { if (debugIconContinueColor) { collector.addRule(`.monaco-workbench .codicon-debug-continue { color: ${debugIconContinueColor} !important; }`); } + + const debugIconStepBackColor = theme.getColor(debugIconStepBackForeground); + if (debugIconStepBackColor) { + collector.addRule(`.monaco-workbench .codicon-debug-step-back { color: ${debugIconStepBackColor} !important; }`); + } }); diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-conditional.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-conditional.svg deleted file mode 100644 index 382507ebcdf..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-conditional.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-data-disabled.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-data-disabled.svg deleted file mode 100644 index a2c8c3417e5..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-data-disabled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-data-unverified.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-data-unverified.svg deleted file mode 100644 index 96dda92ee38..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-data-unverified.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-data.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-data.svg deleted file mode 100644 index 6752b060aeb..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-data.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-disabled.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-disabled.svg deleted file mode 100644 index 84588f8eac5..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-disabled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-function-disabled.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-function-disabled.svg deleted file mode 100644 index cd71f6e462e..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-function-disabled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-function-unverified.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-function-unverified.svg deleted file mode 100644 index 9e2354d67bd..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-function-unverified.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-function.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-function.svg deleted file mode 100644 index f25e57ffde9..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-function.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-hint.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-hint.svg deleted file mode 100644 index d622c6cf0c4..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-hint.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-log-disabled.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-log-disabled.svg deleted file mode 100644 index ea246058e0f..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-log-disabled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-log-unverified.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-log-unverified.svg deleted file mode 100644 index ae8ed0ba7b6..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-log-unverified.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-log.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-log.svg deleted file mode 100644 index fc72afc7e2b..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-log.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-unsupported.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-unsupported.svg deleted file mode 100644 index 624b9f60c80..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-unsupported.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint-unverified.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint-unverified.svg deleted file mode 100644 index 0f39b8b7c8f..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint-unverified.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpoint.svg b/src/vs/workbench/contrib/debug/browser/media/breakpoint.svg deleted file mode 100644 index af02a874950..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/breakpoint.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/continue-dark.svg b/src/vs/workbench/contrib/debug/browser/media/continue-dark.svg deleted file mode 100644 index 7c58386cccc..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/continue-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/continue-light.svg b/src/vs/workbench/contrib/debug/browser/media/continue-light.svg deleted file mode 100644 index 7c58386cccc..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/continue-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/current-and-breakpoint.svg b/src/vs/workbench/contrib/debug/browser/media/current-and-breakpoint.svg deleted file mode 100644 index 17d71fddac9..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/current-and-breakpoint.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/current-arrow.svg b/src/vs/workbench/contrib/debug/browser/media/current-arrow.svg deleted file mode 100644 index 85f288efcd3..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/current-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index fd184092abd..b7129400782 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -3,47 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* Activity Bar */ -.monaco-editor .debug-top-stack-frame-column::before { - background: url('current-arrow.svg') center center no-repeat; -} - -.debug-breakpoint-hint { - background: url('breakpoint-hint.svg') center center no-repeat; +.codicon-debug-hint { cursor: pointer; } -.debug-breakpoint-disabled, -.monaco-editor .inline-breakpoint-widget.debug-breakpoint-disabled { - background: url('breakpoint-disabled.svg') center center no-repeat; +.codicon-debug-hint:not(*[class*='codicon-debug-breakpoint']) { + opacity: .4 !important; } -.monaco-editor .inline-breakpoint-widget.debug-breakpoint-disabled:hover { - background: url('breakpoint-hint.svg') center center no-repeat; +/* overlapped icons */ +.inline-breakpoint-widget.codicon-debug-breakpoint-stackframe-dot::after { + position: absolute; + top: 0; + left: 0; +} + +.codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe-focused::after { + position: absolute; +} + +.inline-breakpoint-widget.codicon-debug-breakpoint-stackframe-dot::after { + content: "\eb8b"; +} + +.codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe-focused::after { + content: "\eb8a"; } .monaco-editor .inline-breakpoint-widget.line-start { left: -0.45em !important; } -.debug-breakpoint-unverified, -.monaco-editor .inline-breakpoint-widget.debug-breakpoint-unverified { - background: url('breakpoint-unverified.svg') center center no-repeat; -} - -.monaco-editor .debug-top-stack-frame { - background: url('current-arrow.svg') center center no-repeat; -} - -.monaco-editor .debug-focused-stack-frame { - background: url('stackframe-arrow.svg') center center no-repeat; -} - -.debug-breakpoint, -.monaco-editor .inline-breakpoint-widget { - background: url('breakpoint.svg') center center no-repeat; -} - .monaco-editor .debug-breakpoint-placeholder::before, .monaco-editor .debug-top-stack-frame-column::before { content: " "; @@ -70,67 +60,6 @@ cursor: pointer; } -.debug-function-breakpoint { - background: url('breakpoint-function.svg') center center no-repeat; -} - -.debug-function-breakpoint-unverified { - background: url('breakpoint-function-unverified.svg') center center no-repeat; -} - -.debug-function-breakpoint-disabled { - background: url('breakpoint-function-disabled.svg') center center no-repeat; -} - -.debug-data-breakpoint { - background: url('breakpoint-data.svg') center center no-repeat; -} - -.debug-data-breakpoint-unverified { - background: url('breakpoint-data-unverified.svg') center center no-repeat; -} - -.debug-data-breakpoint-disabled { - background: url('breakpoint-data-disabled.svg') center center no-repeat; -} - -.debug-breakpoint-conditional, -.monaco-editor .inline-breakpoint-widget.debug-breakpoint-conditional { - background: url('breakpoint-conditional.svg') center center no-repeat; -} - -.debug-breakpoint-log, -.monaco-editor .inline-breakpoint-widget.debug-breakpoint-log { - background: url('breakpoint-log.svg') center center no-repeat; -} - -.debug-breakpoint-log-disabled, -.monaco-editor .inline-breakpoint-widget.debug-breakpoint-log-disabled { - background: url('breakpoint-log-disabled.svg') center center no-repeat; -} - -.debug-breakpoint-log-unverified, -.monaco-editor .inline-breakpoint-widget.debug-breakpoint-log-unverified { - background: url('breakpoint-log-unverified.svg') center center no-repeat; -} - -.debug-breakpoint-unsupported, -.monaco-editor .inline-breakpoint-widget.debug-breakpoint-unsupported { - background: url('breakpoint-unsupported.svg') center center no-repeat; -} - -.monaco-editor .debug-top-stack-frame.debug-breakpoint-and-top-stack-frame, -.monaco-editor .debug-breakpoint.debug-top-stack-frame, -.monaco-editor .debug-breakpoint-and-top-stack-frame-at-column { - background: url('current-and-breakpoint.svg') center center no-repeat; -} - -.monaco-editor .debug-focused-stack-frame.debug-breakpoint, -.monaco-editor .debug-focused-stack-frame.debug-breakpoint-conditional, -.monaco-editor .debug-focused-stack-frame.debug-breakpoint-log { - background: url('stackframe-and-breakpoint.svg') center center no-repeat; -} - /* Error editor */ .debug-error-editor:focus { outline: none !important; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css index a0a76da7ec1..4ac6feef480 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css @@ -43,4 +43,7 @@ background-size: 16px; background-position: center center; background-repeat: no-repeat; + display: flex; + align-items: center; + justify-content: center; } diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 9847a8b1d06..185b8187d41 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -78,6 +78,10 @@ color: #666; } +.debug-viewlet .monaco-list:focus .monaco-list-row.selected.focused .codicon { + color: inherit !important; +} + .debug-viewlet .disabled { opacity: 0.35; } @@ -333,10 +337,17 @@ flex-shrink: 0; } -.debug-viewlet .debug-breakpoints .breakpoint > .icon { +.debug-viewlet .debug-breakpoints .breakpoint > .codicon { width: 19px; height: 19px; min-width: 19px; + display: flex; + align-items: center; + justify-content: center; +} + +.debug-viewlet .debug-breakpoints .breakpoint > .codicon-debug-breakpoint-stackframe-dot::before { + content: "\ea71"; } .debug-viewlet .debug-breakpoints .breakpoint > .file-path { diff --git a/src/vs/workbench/contrib/debug/browser/media/disconnect-dark.svg b/src/vs/workbench/contrib/debug/browser/media/disconnect-dark.svg deleted file mode 100644 index 71aae0dd887..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/disconnect-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/disconnect-light.svg b/src/vs/workbench/contrib/debug/browser/media/disconnect-light.svg deleted file mode 100644 index 06fc4c31553..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/disconnect-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/pause-dark.svg b/src/vs/workbench/contrib/debug/browser/media/pause-dark.svg deleted file mode 100644 index 9cd9f466130..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/pause-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/pause-light.svg b/src/vs/workbench/contrib/debug/browser/media/pause-light.svg deleted file mode 100644 index 01d3cbc290c..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/pause-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/restart-dark.svg b/src/vs/workbench/contrib/debug/browser/media/restart-dark.svg deleted file mode 100644 index fc48916d5a6..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/restart-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/restart-light.svg b/src/vs/workbench/contrib/debug/browser/media/restart-light.svg deleted file mode 100644 index 4964d5bfaf1..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/restart-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/reverse-continue-dark.svg b/src/vs/workbench/contrib/debug/browser/media/reverse-continue-dark.svg deleted file mode 100644 index e0bbfb4202e..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/reverse-continue-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/reverse-continue-light.svg b/src/vs/workbench/contrib/debug/browser/media/reverse-continue-light.svg deleted file mode 100644 index e0bbfb4202e..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/reverse-continue-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/stackframe-and-breakpoint.svg b/src/vs/workbench/contrib/debug/browser/media/stackframe-and-breakpoint.svg deleted file mode 100644 index 3ce31c822c7..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/stackframe-and-breakpoint.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/stackframe-arrow.svg b/src/vs/workbench/contrib/debug/browser/media/stackframe-arrow.svg deleted file mode 100644 index 38b63a34c53..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/stackframe-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/step-back-dark.svg b/src/vs/workbench/contrib/debug/browser/media/step-back-dark.svg deleted file mode 100644 index 5a6ada3e113..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/step-back-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/step-back-light.svg b/src/vs/workbench/contrib/debug/browser/media/step-back-light.svg deleted file mode 100644 index b5a994d4252..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/step-back-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/step-into-dark.svg b/src/vs/workbench/contrib/debug/browser/media/step-into-dark.svg deleted file mode 100644 index 570ae02aafa..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/step-into-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/step-into-light.svg b/src/vs/workbench/contrib/debug/browser/media/step-into-light.svg deleted file mode 100644 index 55c47062f5c..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/step-into-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/step-out-dark.svg b/src/vs/workbench/contrib/debug/browser/media/step-out-dark.svg deleted file mode 100644 index 33a7a2fdb72..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/step-out-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/step-out-light.svg b/src/vs/workbench/contrib/debug/browser/media/step-out-light.svg deleted file mode 100644 index 6ac2139659d..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/step-out-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/step-over-dark.svg b/src/vs/workbench/contrib/debug/browser/media/step-over-dark.svg deleted file mode 100644 index 5bf10674eec..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/step-over-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/step-over-light.svg b/src/vs/workbench/contrib/debug/browser/media/step-over-light.svg deleted file mode 100644 index b874a2564b5..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/step-over-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/stop-dark.svg b/src/vs/workbench/contrib/debug/browser/media/stop-dark.svg deleted file mode 100644 index 9a28f77a9f9..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/stop-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/media/stop-light.svg b/src/vs/workbench/contrib/debug/browser/media/stop-light.svg deleted file mode 100644 index 9a28f77a9f9..00000000000 --- a/src/vs/workbench/contrib/debug/browser/media/stop-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 37ed56928be..88b1a920272 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -344,7 +344,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati } focus(): void { - this.replInput.focus(); + setTimeout(() => this.replInput.focus(), 0); } getActionViewItem(action: IAction): IActionViewItem | undefined { diff --git a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts index 56a9de64e26..9cbc4234d65 100644 --- a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts +++ b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts @@ -18,13 +18,11 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { private requestCallback: ((request: DebugProtocol.Request) => void) | undefined; private eventCallback: ((request: DebugProtocol.Event) => void) | undefined; private messageCallback: ((message: DebugProtocol.ProtocolMessage) => void) | undefined; - protected readonly _onError: Emitter; - protected readonly _onExit: Emitter; + protected readonly _onError = new Emitter(); + protected readonly _onExit = new Emitter(); constructor() { this.sequence = 1; - this._onError = new Emitter(); - this._onExit = new Emitter(); } abstract startSession(): Promise; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 0e647c09b23..62586d2f4ce 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -810,9 +810,9 @@ export class DebugModel implements IDebugModel { private toDispose: lifecycle.IDisposable[]; private schedulers = new Map(); private breakpointsActivated = true; - private readonly _onDidChangeBreakpoints: Emitter; - private readonly _onDidChangeCallStack: Emitter; - private readonly _onDidChangeWatchExpressions: Emitter; + private readonly _onDidChangeBreakpoints = new Emitter(); + private readonly _onDidChangeCallStack = new Emitter(); + private readonly _onDidChangeWatchExpressions = new Emitter(); constructor( private breakpoints: Breakpoint[], @@ -824,9 +824,6 @@ export class DebugModel implements IDebugModel { ) { this.sessions = []; this.toDispose = []; - this._onDidChangeBreakpoints = new Emitter(); - this._onDidChangeCallStack = new Emitter(); - this._onDidChangeWatchExpressions = new Emitter(); } getId(): string { diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index f261025e49d..7d0889b5c26 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -17,9 +17,9 @@ export class ViewModel implements IViewModel { private _focusedThread: IThread | undefined; private selectedExpression: IExpression | undefined; private selectedFunctionBreakpoint: IFunctionBreakpoint | undefined; - private readonly _onDidFocusSession: Emitter; - private readonly _onDidFocusStackFrame: Emitter<{ stackFrame: IStackFrame | undefined, explicit: boolean }>; - private readonly _onDidSelectExpression: Emitter; + private readonly _onDidFocusSession = new Emitter(); + private readonly _onDidFocusStackFrame = new Emitter<{ stackFrame: IStackFrame | undefined, explicit: boolean }>(); + private readonly _onDidSelectExpression = new Emitter(); private multiSessionView: boolean; private expressionSelectedContextKey: IContextKey; private breakpointSelectedContextKey: IContextKey; @@ -30,9 +30,6 @@ export class ViewModel implements IViewModel { private jumpToCursorSupported: IContextKey; constructor(contextKeyService: IContextKeyService) { - this._onDidFocusSession = new Emitter(); - this._onDidFocusStackFrame = new Emitter<{ stackFrame: IStackFrame, explicit: boolean }>(); - this._onDidSelectExpression = new Emitter(); this.multiSessionView = false; this.expressionSelectedContextKey = CONTEXT_EXPRESSION_SELECTED.bindTo(contextKeyService); this.breakpointSelectedContextKey = CONTEXT_BREAKPOINT_SELECTED.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts index f3cde426dd0..3d934b3d584 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts @@ -1124,7 +1124,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (context.res.statusCode !== 200) { return Promise.resolve(undefined); } - return asJson(context).then((result: { [key: string]: any }) => { + return asJson(context).then((result: { [key: string]: any } | null) => { if (!result) { return; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 2b847bf7ab5..d3e7af804db 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -59,7 +59,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; export function toExtensionDescription(local: ILocalExtension): IExtensionDescription { @@ -1051,6 +1051,7 @@ export class CheckForUpdatesAction extends Action { @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService, @IViewletService private readonly viewletService: IViewletService, + @IDialogService private readonly dialogService: IDialogService, @INotificationService private readonly notificationService: INotificationService ) { super(id, label, '', true); @@ -1059,7 +1060,7 @@ export class CheckForUpdatesAction extends Action { private checkUpdatesAndNotify(): void { const outdated = this.extensionsWorkbenchService.outdated; if (!outdated.length) { - this.notificationService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); + this.dialogService.show(Severity.Info, localize('noUpdatesAvailable', "All extensions are up to date."), [localize('ok', "OK")]); return; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index 8c115cdd912..6f07908168a 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -212,6 +212,7 @@ .extensions-viewlet > .extensions .extension > .details > .header-container > .header .codicon { font-size: 120%; margin-right: 2px; + -webkit-mask: inherit; } .extensions-viewlet > .extensions .extension > .details > .header-container > .header > .ratings { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 57456d500ea..032d8c5ddcd 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -6,11 +6,11 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL } from 'vs/workbench/contrib/files/browser/fileActions'; -import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/saveErrorHandler'; +import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/textFileSaveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, DirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, DirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, SaveableEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -18,7 +18,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService, ExplorerResourceMoveableToTrash, ExplorerViewletVisibleContext } from 'vs/workbench/contrib/files/common/files'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from 'vs/workbench/browser/actions/workspaceCommands'; import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { AutoSaveContext } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { URI } from 'vs/base/common/uri'; @@ -26,7 +26,7 @@ import { Schemas } from 'vs/base/common/network'; import { WorkspaceFolderCountContext, IsWebContext } from 'vs/workbench/browser/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { ActiveEditorIsSaveableContext } from 'vs/workbench/common/editor'; +import { ActiveEditorIsSaveableContext, DirtyWorkingCopiesContext } from 'vs/workbench/common/editor'; import { SidebarFocusContext } from 'vs/workbench/common/viewlet'; import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; @@ -44,7 +44,6 @@ registry.registerWorkbenchAction(SyncActionDescriptor.create(GlobalNewUntitledFi registry.registerWorkbenchAction(SyncActionDescriptor.create(CompareWithClipboardAction, CompareWithClipboardAction.ID, CompareWithClipboardAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleAutoSaveAction, ToggleAutoSaveAction.ID, ToggleAutoSaveAction.LABEL), 'File: Toggle Auto Save', category.value); - const workspacesCategory = nls.localize('workspaces', "Workspaces"); registry.registerWorkbenchAction(SyncActionDescriptor.create(OpenWorkspaceAction, OpenWorkspaceAction.ID, OpenWorkspaceAction.LABEL), 'Workspaces: Open Workspace...', workspacesCategory); @@ -242,7 +241,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', order: 10, command: openToSideCommand, - when: ResourceContextKey.IsFileSystemResource + when: ContextKeyExpr.or(ResourceContextKey.IsFileSystemResource, ResourceContextKey.Scheme.isEqualTo(Schemas.untitled)) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -267,7 +266,19 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: SAVE_FILE_LABEL, precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) + when: ContextKeyExpr.or( + // Untitled Editors + ResourceContextKey.Scheme.isEqualTo(Schemas.untitled), + // Or: + ContextKeyExpr.and( + // Not: editor groups + OpenEditorsGroupContext.toNegated(), + // Not: readonly editors + SaveableEditorContext, + // Not: auto save after short delay + AutoSaveAfterShortDelayContext.toNegated() + ) + ) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -278,25 +289,28 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: nls.localize('revert', "Revert File"), precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) -}); - -MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { - group: '2_save', - command: { - id: SAVE_FILE_AS_COMMAND_ID, - title: SAVE_FILE_AS_LABEL - }, - when: ResourceContextKey.Scheme.isEqualTo(Schemas.untitled) + when: ContextKeyExpr.and( + // Not: editor groups + OpenEditorsGroupContext.toNegated(), + // Not: readonly editors + SaveableEditorContext, + // Not: untitled editors (revert closes them) + ResourceContextKey.Scheme.notEqualsTo(Schemas.untitled), + // Not: auto save after short delay + AutoSaveAfterShortDelayContext.toNegated() + ) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '2_save', + order: 30, command: { id: SAVE_ALL_IN_GROUP_COMMAND_ID, - title: nls.localize('saveAll', "Save All") + title: nls.localize('saveAll', "Save All"), + precondition: DirtyWorkingCopiesContext }, - when: ContextKeyExpr.and(OpenEditorsGroupContext, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) + // Editor Group + when: OpenEditorsGroupContext }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -307,7 +321,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: nls.localize('compareWithSaved', "Compare with Saved"), precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo(''), WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveAfterShortDelayContext.toNegated(), WorkbenchListDoubleSelection.toNegated()) }); const compareResourceCommand = { @@ -585,7 +599,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '4_save', command: { id: SaveAllAction.ID, - title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll") + title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll"), + precondition: DirtyWorkingCopiesContext }, order: 3 }); @@ -642,7 +657,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '6_close', command: { id: REVERT_FILE_COMMAND_ID, - title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File") + title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File"), + precondition: ContextKeyExpr.or(ActiveEditorIsSaveableContext, ContextKeyExpr.and(ExplorerViewletVisibleContext, SidebarFocusContext)) }, order: 1 }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 6386b86db0c..72d5dc4669a 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -31,7 +31,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IListService, ListWidget } from 'vs/platform/list/browser/listService'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Schemas } from 'vs/base/common/network'; import { IDialogService, IConfirmationResult, getConfirmMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -40,13 +39,12 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Constants } from 'vs/base/common/uint'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { coalesce } from 'vs/base/common/arrays'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { onUnexpectedError, getErrorMessage } from 'vs/base/common/errors'; import { asDomUri, triggerDownload } from 'vs/base/browser/dom'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -529,11 +527,11 @@ export abstract class BaseSaveAllAction extends Action { private registerListeners(): void { // update enablement based on working copy changes - this._register(this.workingCopyService.onDidChangeDirty(() => this.updateEnablement())); + this._register(this.workingCopyService.onDidChangeDirty(w => this.updateEnablement(w))); } - private updateEnablement(): void { - const hasDirty = this.workingCopyService.hasDirty; + private updateEnablement(workingCopy: IWorkingCopy): void { + const hasDirty = workingCopy.isDirty() || this.workingCopyService.hasDirty; if (this.lastIsDirty !== hasDirty) { this.enabled = hasDirty; this.lastIsDirty = this.enabled; @@ -840,22 +838,6 @@ class ClipboardContentProvider implements ITextModelContentProvider { } } -interface IExplorerContext { - stat?: ExplorerItem; - selection: ExplorerItem[]; -} - -function getContext(listWidget: ListWidget): IExplorerContext { - // These commands can only be triggered when explorer viewlet is visible so get it using the active viewlet - const tree = >listWidget; - const focus = tree.getFocus(); - const stat = focus.length ? focus[0] : undefined; - const selection = tree.getSelection(); - - // Only respect the selection if user clicked inside it (focus belongs to it) - return { stat, selection: selection && typeof stat !== 'undefined' && selection.indexOf(stat) >= 0 ? selection : [] }; -} - function onErrorWithRetry(notificationService: INotificationService, error: any, retry: () => Promise): void { notificationService.prompt(Severity.Error, toErrorMessage(error, false), [{ @@ -866,7 +848,6 @@ function onErrorWithRetry(notificationService: INotificationService, error: any, } async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boolean): Promise { - const listService = accessor.get(IListService); const explorerService = accessor.get(IExplorerService); const fileService = accessor.get(IFileService); const textFileService = accessor.get(ITextFileService); @@ -876,47 +857,45 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole await viewletService.openViewlet(VIEWLET_ID, true); - const list = listService.lastFocusedList; - if (list) { - const { stat } = getContext(list); - let folder: ExplorerItem; - if (stat) { - folder = stat.isDirectory ? stat : (stat.parent || explorerService.roots[0]); - } else { - folder = explorerService.roots[0]; - } - - if (folder.isReadonly) { - throw new Error('Parent folder is readonly.'); - } - - const newStat = new NewExplorerItem(folder, isFolder); - await folder.fetchChildren(fileService, explorerService); - - folder.addChild(newStat); - - const onSuccess = (value: string): Promise => { - const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : textFileService.create(resources.joinPath(folder.resource, value)); - return createPromise.then(created => { - refreshIfSeparator(value, explorerService); - return isFolder ? explorerService.select(created.resource, true) - : editorService.openEditor({ resource: created.resource, options: { pinned: true } }).then(() => undefined); - }, error => { - onErrorWithRetry(notificationService, error, () => onSuccess(value)); - }); - }; - - explorerService.setEditable(newStat, { - validationMessage: value => validateFileName(newStat, value), - onFinish: (value, success) => { - folder.removeChild(newStat); - explorerService.setEditable(newStat, null); - if (success) { - onSuccess(value); - } - } - }); + const stats = explorerService.getContext(false); + const stat = stats.length > 0 ? stats[0] : undefined; + let folder: ExplorerItem; + if (stat) { + folder = stat.isDirectory ? stat : (stat.parent || explorerService.roots[0]); + } else { + folder = explorerService.roots[0]; } + + if (folder.isReadonly) { + throw new Error('Parent folder is readonly.'); + } + + const newStat = new NewExplorerItem(folder, isFolder); + await folder.fetchChildren(fileService, explorerService); + + folder.addChild(newStat); + + const onSuccess = (value: string): Promise => { + const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : textFileService.create(resources.joinPath(folder.resource, value)); + return createPromise.then(created => { + refreshIfSeparator(value, explorerService); + return isFolder ? explorerService.select(created.resource, true) + : editorService.openEditor({ resource: created.resource, options: { pinned: true } }).then(() => undefined); + }, error => { + onErrorWithRetry(notificationService, error, () => onSuccess(value)); + }); + }; + + explorerService.setEditable(newStat, { + validationMessage: value => validateFileName(newStat, value), + onFinish: (value, success) => { + folder.removeChild(newStat); + explorerService.setEditable(newStat, null); + if (success) { + onSuccess(value); + } + } + }); } CommandsRegistry.registerCommand({ @@ -934,14 +913,11 @@ CommandsRegistry.registerCommand({ }); export const renameHandler = (accessor: ServicesAccessor) => { - const listService = accessor.get(IListService); const explorerService = accessor.get(IExplorerService); const textFileService = accessor.get(ITextFileService); - if (!listService.lastFocusedList) { - return; - } - const { stat } = getContext(listService.lastFocusedList); + const stats = explorerService.getContext(false); + const stat = stats.length > 0 ? stats[0] : undefined; if (!stat) { return; } @@ -962,51 +938,32 @@ export const renameHandler = (accessor: ServicesAccessor) => { }; export const moveFileToTrashHandler = (accessor: ServicesAccessor) => { - const listService = accessor.get(IListService); - if (!listService.lastFocusedList) { - return Promise.resolve(); - } - const explorerContext = getContext(listService.lastFocusedList); - const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!]; - + const explorerService = accessor.get(IExplorerService); + const stats = explorerService.getContext(true); return deleteFiles(accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFileService), stats, true); }; export const deleteFileHandler = (accessor: ServicesAccessor) => { - const listService = accessor.get(IListService); - if (!listService.lastFocusedList) { - return Promise.resolve(); - } - const explorerContext = getContext(listService.lastFocusedList); - const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!]; + const explorerService = accessor.get(IExplorerService); + const stats = explorerService.getContext(true); return deleteFiles(accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFileService), stats, false); }; let pasteShouldMove = false; export const copyFileHandler = (accessor: ServicesAccessor) => { - const listService = accessor.get(IListService); - if (!listService.lastFocusedList) { - return; - } - const explorerContext = getContext(listService.lastFocusedList); const explorerService = accessor.get(IExplorerService); - if (explorerContext.stat) { - const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; + const stats = explorerService.getContext(true); + if (stats.length > 0) { explorerService.setToCopy(stats, false); pasteShouldMove = false; } }; export const cutFileHandler = (accessor: ServicesAccessor) => { - const listService = accessor.get(IListService); - if (!listService.lastFocusedList) { - return; - } - const explorerContext = getContext(listService.lastFocusedList); const explorerService = accessor.get(IExplorerService); - if (explorerContext.stat) { - const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; + const stats = explorerService.getContext(true); + if (stats.length > 0) { explorerService.setToCopy(stats, true); pasteShouldMove = true; } @@ -1014,47 +971,41 @@ export const cutFileHandler = (accessor: ServicesAccessor) => { export const DOWNLOAD_COMMAND_ID = 'explorer.download'; const downloadFileHandler = (accessor: ServicesAccessor) => { - const listService = accessor.get(IListService); - if (!listService.lastFocusedList) { - return; - } - const explorerContext = getContext(listService.lastFocusedList); const fileService = accessor.get(IFileService); const fileDialogService = accessor.get(IFileDialogService); + const explorerService = accessor.get(IExplorerService); + const stats = explorerService.getContext(true); - if (explorerContext.stat) { - const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; - stats.forEach(async s => { - if (isWeb) { - if (!s.isDirectory) { - triggerDownload(asDomUri(s.resource), s.name); - } - } else { - let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath() : fileDialogService.defaultFilePath(); - if (defaultUri && !s.isDirectory) { - defaultUri = resources.joinPath(defaultUri, s.name); - } - - const destination = await fileDialogService.showSaveDialog({ - availableFileSystems: [Schemas.file], - saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")), - title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), - defaultUri - }); - if (destination) { - await fileService.copy(s.resource, destination); - } + stats.forEach(async s => { + if (isWeb) { + if (!s.isDirectory) { + triggerDownload(asDomUri(s.resource), s.name); } - }); - } + } else { + let defaultUri = s.isDirectory ? fileDialogService.defaultFolderPath() : fileDialogService.defaultFilePath(); + if (defaultUri && !s.isDirectory) { + defaultUri = resources.joinPath(defaultUri, s.name); + } + + const destination = await fileDialogService.showSaveDialog({ + availableFileSystems: [Schemas.file], + saveLabel: mnemonicButtonLabel(nls.localize('download', "Download")), + title: s.isDirectory ? nls.localize('downloadFolder', "Download Folder") : nls.localize('downloadFile', "Download File"), + defaultUri + }); + if (destination) { + await fileService.copy(s.resource, destination); + } + } + }); }; + CommandsRegistry.registerCommand({ id: DOWNLOAD_COMMAND_ID, handler: downloadFileHandler }); export const pasteFileHandler = async (accessor: ServicesAccessor) => { - const listService = accessor.get(IListService); const clipboardService = accessor.get(IClipboardService); const explorerService = accessor.get(IExplorerService); const fileService = accessor.get(IFileService); @@ -1063,72 +1014,65 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); const configurationService = accessor.get(IConfigurationService); - if (listService.lastFocusedList) { - const explorerContext = getContext(listService.lastFocusedList); - const toPaste = resources.distinctParents(clipboardService.readResources(), r => r); - const element = explorerContext.stat || explorerService.roots[0]; + const context = explorerService.getContext(true); + const toPaste = resources.distinctParents(clipboardService.readResources(), r => r); + const element = context.length ? context[0] : explorerService.roots[0]; - // Check if target is ancestor of pasted folder - const stats = await Promise.all(toPaste.map(async fileToPaste => { + // Check if target is ancestor of pasted folder + const stats = await Promise.all(toPaste.map(async fileToPaste => { - if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) { - throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder")); - } - - try { - const fileToPasteStat = await fileService.resolve(fileToPaste); - - // Find target - let target: ExplorerItem; - if (element.resource.toString() === fileToPaste.toString()) { - target = element.parent!; - } else { - target = element.isDirectory ? element : element.parent!; - } - - const incrementalNaming = configurationService.getValue().explorer.incrementalNaming; - const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming); - - // Move/Copy File - if (pasteShouldMove) { - return await textFileService.move(fileToPaste, targetFile); - } else { - return await fileService.copy(fileToPaste, targetFile); - } - } catch (e) { - onError(notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile. {0}", getErrorMessage(e)))); - return undefined; - } - })); - - if (pasteShouldMove) { - // Cut is done. Make sure to clear cut state. - explorerService.setToCopy([], false); + if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) { + throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder")); } - if (stats.length >= 1) { - const stat = stats[0]; - if (stat && !stat.isDirectory && stats.length === 1) { - await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } }); + + try { + const fileToPasteStat = await fileService.resolve(fileToPaste); + + // Find target + let target: ExplorerItem; + if (element.resource.toString() === fileToPaste.toString()) { + target = element.parent!; + } else { + target = element.isDirectory ? element : element.parent!; } - if (stat) { - await explorerService.select(stat.resource); + + const incrementalNaming = configurationService.getValue().explorer.incrementalNaming; + const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming); + + // Move/Copy File + if (pasteShouldMove) { + return await textFileService.move(fileToPaste, targetFile); + } else { + return await fileService.copy(fileToPaste, targetFile); } + } catch (e) { + onError(notificationService, new Error(nls.localize('fileDeleted', "File to paste was deleted or moved meanwhile. {0}", getErrorMessage(e)))); + return undefined; + } + })); + + if (pasteShouldMove) { + // Cut is done. Make sure to clear cut state. + explorerService.setToCopy([], false); + } + if (stats.length >= 1) { + const stat = stats[0]; + if (stat && !stat.isDirectory && stats.length === 1) { + await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } }); + } + if (stat) { + await explorerService.select(stat.resource); } } }; export const openFilePreserveFocusHandler = async (accessor: ServicesAccessor) => { - const listService = accessor.get(IListService); const editorService = accessor.get(IEditorService); + const explorerService = accessor.get(IExplorerService); + const stats = explorerService.getContext(true); - if (listService.lastFocusedList) { - const explorerContext = getContext(listService.lastFocusedList); - if (explorerContext.stat) { - const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat]; - await editorService.openEditors(stats.filter(s => !s.isDirectory).map(s => ({ - resource: s.resource, - options: { preserveFocus: true } - }))); - } - } + await editorService.openEditors(stats.filter(s => !s.isDirectory).map(s => ({ + resource: s.resource, + options: { preserveFocus: true } + }))); }; diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index e6192c5c094..a3c9bae7a01 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -5,44 +5,37 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { toResource, IEditorCommandsContext, SideBySideEditor } from 'vs/workbench/common/editor'; +import { toResource, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; import { IWindowOpenable, IOpenWindowOptions, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, FilesExplorerFocusCondition } from 'vs/workbench/contrib/files/common/files'; import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IListService } from 'vs/platform/list/browser/listService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceInput } from 'vs/platform/editor/common/editor'; +import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { IEditorViewState } from 'vs/editor/common/editorCommon'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { isWindows } from 'vs/base/common/platform'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { getResourceForCommand, getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files'; +import { getResourceForCommand, getMultiSelectedResources, getMultiSelectedEditors } from 'vs/workbench/contrib/files/browser/files'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { getMultiSelectedEditorContexts } from 'vs/workbench/browser/parts/editor/editorCommands'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService, SIDE_GROUP, ISaveEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService, GroupsOrder, EditorsOrder, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { basename, toLocalResource, joinPath, isEqual } from 'vs/base/common/resources'; +import { basename, joinPath, isEqual } from 'vs/base/common/resources'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { UNTITLED_WORKSPACE_NAME } from 'vs/platform/workspaces/common/workspaces'; -import { withUndefinedAsNull, withNullAsUndefined } from 'vs/base/common/types'; // Commands @@ -73,11 +66,17 @@ export const SAVE_FILES_COMMAND_ID = 'workbench.action.files.saveFiles'; export const OpenEditorsGroupContext = new RawContextKey('groupFocusedInOpenEditors', false); export const DirtyEditorContext = new RawContextKey('dirtyEditor', false); +export const SaveableEditorContext = new RawContextKey('saveableEditor', false); export const ResourceSelectedForCompareContext = new RawContextKey('resourceSelectedForCompare', false); export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder'; export const REMOVE_ROOT_FOLDER_LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace"); +export const PREVIOUS_COMPRESSED_FOLDER = 'previousCompressedFolder'; +export const NEXT_COMPRESSED_FOLDER = 'nextCompressedFolder'; +export const FIRST_COMPRESSED_FOLDER = 'firstCompressedFolder'; +export const LAST_COMPRESSED_FOLDER = 'lastCompressedFolder'; + export const openWindowCommand = (accessor: ServicesAccessor, toOpen: IWindowOpenable[], options?: IOpenWindowOptions) => { if (Array.isArray(toOpen)) { const hostService = accessor.get(IHostService); @@ -103,186 +102,8 @@ export const newWindowCommand = (accessor: ServicesAccessor, options?: IOpenEmpt hostService.openWindow(options); }; -async function save( - resource: URI | null, - isSaveAs: boolean, - options: ISaveOptions | undefined, - editorService: IEditorService, - fileService: IFileService, - untitledTextEditorService: IUntitledTextEditorService, - textFileService: ITextFileService, - editorGroupService: IEditorGroupsService, - environmentService: IWorkbenchEnvironmentService -): Promise { - if (!resource || (!fileService.canHandleResource(resource) && resource.scheme !== Schemas.untitled)) { - return; // save is not supported - } - - // Save As (or Save untitled with associated path) - if (isSaveAs || resource.scheme === Schemas.untitled) { - return doSaveAs(resource, isSaveAs, options, editorService, fileService, untitledTextEditorService, textFileService, editorGroupService, environmentService); - } - - // Save - return doSave(resource, options, editorService, textFileService); -} - -async function doSaveAs( - resource: URI, - isSaveAs: boolean, - options: ISaveOptions | undefined, - editorService: IEditorService, - fileService: IFileService, - untitledTextEditorService: IUntitledTextEditorService, - textFileService: ITextFileService, - editorGroupService: IEditorGroupsService, - environmentService: IWorkbenchEnvironmentService -): Promise { - let viewStateOfSource: IEditorViewState | undefined = undefined; - const activeTextEditorWidget = getCodeEditor(editorService.activeTextEditorWidget); - if (activeTextEditorWidget) { - const activeResource = toResource(editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); - if (activeResource && (fileService.canHandleResource(activeResource) || resource.scheme === Schemas.untitled) && isEqual(activeResource, resource)) { - viewStateOfSource = withNullAsUndefined(activeTextEditorWidget.saveViewState()); - } - } - - // Special case: an untitled file with associated path gets saved directly unless "saveAs" is true - let target: URI | undefined; - if (!isSaveAs && resource.scheme === Schemas.untitled && untitledTextEditorService.hasAssociatedFilePath(resource)) { - const result = await textFileService.save(resource, options); - if (result) { - target = toLocalResource(resource, environmentService.configuration.remoteAuthority); - } - } - - // Otherwise, really "Save As..." - else { - - // Force a change to the file to trigger external watchers if any - // fixes https://github.com/Microsoft/vscode/issues/59655 - options = ensureForcedSave(options); - - target = await textFileService.saveAs(resource, undefined, options); - } - - if (!target || isEqual(target, resource)) { - return false; // save canceled or same resource used - } - - const replacement: IResourceInput = { - resource: target, - options: { - pinned: true, - viewState: viewStateOfSource - } - }; - - await Promise.all(editorGroupService.groups.map(group => - editorService.replaceEditors([{ - editor: { resource }, - replacement - }], group))); - - return true; -} - -async function doSave( - resource: URI, - options: ISaveOptions | undefined, - editorService: IEditorService, - textFileService: ITextFileService -): Promise { - - // Pin the active editor if we are saving it - const activeControl = editorService.activeControl; - const activeEditorResource = activeControl?.input?.getResource(); - if (activeControl && activeEditorResource && isEqual(activeEditorResource, resource)) { - activeControl.group.pinEditor(activeControl.input); - } - - // Just save (force a change to the file to trigger external watchers if any) - options = ensureForcedSave(options); - - return textFileService.save(resource, options); -} - -function ensureForcedSave(options?: ISaveOptions): ISaveOptions { - if (!options) { - options = { force: true }; - } else { - options.force = true; - } - - return options; -} - -async function saveAll(saveAllArguments: any, editorService: IEditorService, untitledTextEditorService: IUntitledTextEditorService, - textFileService: ITextFileService, editorGroupService: IEditorGroupsService): Promise { - - // Store some properties per untitled file to restore later after save is completed - const groupIdToUntitledResourceInput = new Map(); - - editorGroupService.groups.forEach(group => { - const activeEditorResource = group.activeEditor && group.activeEditor.getResource(); - group.editors.forEach(e => { - const resource = e.getResource(); - if (resource && untitledTextEditorService.isDirty(resource)) { - if (!groupIdToUntitledResourceInput.has(group.id)) { - groupIdToUntitledResourceInput.set(group.id, []); - } - - groupIdToUntitledResourceInput.get(group.id)!.push({ - encoding: untitledTextEditorService.getEncoding(resource), - resource, - options: { - inactive: activeEditorResource ? !isEqual(activeEditorResource, resource) : true, - pinned: true, - preserveFocus: true, - index: group.getIndexOfEditor(e) - } - }); - } - }); - }); - - // Save all - const result = await textFileService.saveAll(saveAllArguments); - - // Update untitled resources to the saved ones, so we open the proper files - groupIdToUntitledResourceInput.forEach((inputs, groupId) => { - inputs.forEach(i => { - const targetResult = result.results.filter(r => r.success && isEqual(r.source, i.resource)).pop(); - if (targetResult?.target) { - i.resource = targetResult.target; - } - }); - - editorService.openEditors(inputs, groupId); - }); -} - // Command registration -CommandsRegistry.registerCommand({ - id: REVERT_FILE_COMMAND_ID, - handler: async (accessor, resource: URI | object) => { - const editorService = accessor.get(IEditorService); - const textFileService = accessor.get(ITextFileService); - const notificationService = accessor.get(INotificationService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService) - .filter(resource => resource.scheme !== Schemas.untitled); - - if (resources.length) { - try { - await textFileService.revertAll(resources, { force: true }); - } catch (error) { - notificationService.error(nls.localize('genericRevertError', "Failed to revert '{0}': {1}", resources.map(r => basename(r)).join(', '), toErrorMessage(error, false))); - } - } - } -}); - KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingWeight.WorkbenchContrib, when: ExplorerFocusCondition, @@ -298,10 +119,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // Set side input if (resources.length) { - const resolved = await fileService.resolveAll(resources.map(resource => ({ resource }))); + const untitledResources = resources.filter(resource => resource.scheme === Schemas.untitled); + const fileResources = resources.filter(resource => resource.scheme !== Schemas.untitled); + + const resolved = await fileService.resolveAll(fileResources.map(resource => ({ resource }))); const editors = resolved.filter(r => r.stat && r.success && !r.stat.isDirectory).map(r => ({ resource: r.stat!.resource - })); + })).concat(...untitledResources.map(untitledResource => ({ resource: untitledResource }))); await editorService.openEditors(editors, SIDE_GROUP); } @@ -483,38 +307,48 @@ CommandsRegistry.registerCommand({ } }); -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: SAVE_FILE_AS_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S, - handler: (accessor, resourceOrObject: URI | object | { from: string }) => { - const editorService = accessor.get(IEditorService); - let resource: URI | null = null; - if (resourceOrObject && 'from' in resourceOrObject && resourceOrObject.from === 'menu') { - resource = withUndefinedAsNull(toResource(editorService.activeEditor)); - } else { - resource = withUndefinedAsNull(getResourceForCommand(resourceOrObject, accessor.get(IListService), editorService)); - } +// Save / Save As / Save All / Revert - return save(resource, true, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IWorkbenchEnvironmentService)); +function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEditorsOptions): Promise { + const listService = accessor.get(IListService); + const editorGroupsService = accessor.get(IEditorGroupsService); + + const saveableEditors = getMultiSelectedEditors(listService, editorGroupsService).filter(({ editor }) => !editor.isReadonly()); + + return doSaveEditors(accessor, saveableEditors, options); +} + +function saveEditorsOfGroups(accessor: ServicesAccessor, groups = accessor.get(IEditorGroupsService).getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE), options?: ISaveEditorsOptions): Promise { + const saveableEditors: IEditorIdentifier[] = []; + for (const group of groups) { + for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + if (editor.isDirty()) { + saveableEditors.push({ groupId: group.id, editor }); + } + } } -}); + + return doSaveEditors(accessor, saveableEditors, options); +} + +async function doSaveEditors(accessor: ServicesAccessor, editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { + const editorService = accessor.get(IEditorService); + const notificationService = accessor.get(INotificationService); + + try { + await editorService.save(editors, options); + } catch (error) { + notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); + } +} KeybindingsRegistry.registerCommandAndKeybindingRule({ when: undefined, weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KEY_S, id: SAVE_FILE_COMMAND_ID, - handler: (accessor, resource: URI | object) => { - const editorService = accessor.get(IEditorService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService); - - if (resources.length === 1) { - // If only one resource is selected explictly call save since the behavior is a bit different than save all #41841 - return save(resources[0], false, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IWorkbenchEnvironmentService)); - } - return saveAll(resources, editorService, accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); + handler: accessor => { + return saveSelectedEditors(accessor, { force: true }); } }); @@ -525,56 +359,87 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S) }, id: SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, handler: accessor => { - const editorService = accessor.get(IEditorService); + return saveSelectedEditors(accessor, { force: true, skipSaveParticipants: true }); + } +}); - const resource = toResource(editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); - if (resource) { - return save(resource, false, { skipSaveParticipants: true }, editorService, accessor.get(IFileService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IWorkbenchEnvironmentService)); - } - - return undefined; +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: SAVE_FILE_AS_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S, + handler: accessor => { + return saveSelectedEditors(accessor, { saveAs: true }); } }); CommandsRegistry.registerCommand({ id: SAVE_ALL_COMMAND_ID, handler: (accessor) => { - return saveAll(true, accessor.get(IEditorService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); + return saveEditorsOfGroups(accessor); } }); CommandsRegistry.registerCommand({ id: SAVE_ALL_IN_GROUP_COMMAND_ID, handler: (accessor, _: URI | object, editorContext: IEditorCommandsContext) => { - const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService), accessor.get(IEditorGroupsService)); const editorGroupService = accessor.get(IEditorGroupsService); - let saveAllArg: any; + + const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService), accessor.get(IEditorGroupsService)); + + let groups: IEditorGroup[] | undefined = undefined; if (!contexts.length) { - saveAllArg = true; + groups = [...editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)]; } else { - const fileService = accessor.get(IFileService); - saveAllArg = []; contexts.forEach(context => { const editorGroup = editorGroupService.getGroup(context.groupId); if (editorGroup) { - editorGroup.editors.forEach(editor => { - const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); - if (resource && (resource.scheme === Schemas.untitled || fileService.canHandleResource(resource))) { - saveAllArg.push(resource); - } - }); + if (!groups) { + groups = []; + } + + groups.push(editorGroup); } }); } - return saveAll(saveAllArg, accessor.get(IEditorService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); + return saveEditorsOfGroups(accessor, groups); } }); CommandsRegistry.registerCommand({ id: SAVE_FILES_COMMAND_ID, - handler: (accessor) => { - return saveAll(false, accessor.get(IEditorService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); + handler: accessor => { + const editorService = accessor.get(IEditorService); + + return editorService.saveAll({ includeUntitled: false }); + } +}); + +CommandsRegistry.registerCommand({ + id: REVERT_FILE_COMMAND_ID, + handler: async accessor => { + const notificationService = accessor.get(INotificationService); + const listService = accessor.get(IListService); + const editorGroupsService = accessor.get(IEditorGroupsService); + + const editors = getMultiSelectedEditors(listService, editorGroupsService); + if (editors.length) { + try { + await Promise.all(editors.map(async ({ groupId, editor }) => { + if (editor.isUntitled()) { + return; // we do not allow to revert untitled editors + } + + // Use revert as a hint to pin the editor + editorGroupsService.getGroup(groupId)?.pinEditor(editor); + + return editor.revert({ force: true }); + })); + } catch (error) { + notificationService.error(nls.localize('genericRevertError', "Failed to revert '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); + } + } } }); @@ -592,3 +457,81 @@ CommandsRegistry.registerCommand({ return workspaceEditingService.removeFolders(resources); } }); + +// Compressed item navigation + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext.negate()), + primary: KeyCode.LeftArrow, + id: PREVIOUS_COMPRESSED_FOLDER, + handler: (accessor) => { + const viewletService = accessor.get(IViewletService); + const viewlet = viewletService.getActiveViewlet(); + + if (viewlet?.getId() !== VIEWLET_ID) { + return; + } + + const explorer = viewlet as ExplorerViewlet; + const view = explorer.getExplorerView(); + view.previousCompressedStat(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedLastFocusContext.negate()), + primary: KeyCode.RightArrow, + id: NEXT_COMPRESSED_FOLDER, + handler: (accessor) => { + const viewletService = accessor.get(IViewletService); + const viewlet = viewletService.getActiveViewlet(); + + if (viewlet?.getId() !== VIEWLET_ID) { + return; + } + + const explorer = viewlet as ExplorerViewlet; + const view = explorer.getExplorerView(); + view.nextCompressedStat(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext.negate()), + primary: KeyCode.Home, + id: FIRST_COMPRESSED_FOLDER, + handler: (accessor) => { + const viewletService = accessor.get(IViewletService); + const viewlet = viewletService.getActiveViewlet(); + + if (viewlet?.getId() !== VIEWLET_ID) { + return; + } + + const explorer = viewlet as ExplorerViewlet; + const view = explorer.getExplorerView(); + view.firstCompressedStat(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerCompressedFocusContext, ExplorerCompressedLastFocusContext.negate()), + primary: KeyCode.End, + id: LAST_COMPRESSED_FOLDER, + handler: (accessor) => { + const viewletService = accessor.get(IViewletService); + const viewlet = viewletService.getActiveViewlet(); + + if (viewlet?.getId() !== VIEWLET_ID) { + return; + } + + const explorer = viewlet as ExplorerViewlet; + const view = explorer.getExplorerView(); + view.lastCompressedStat(); + } +}); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 59fba1cfeaf..ee0dfa24cd3 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -16,7 +16,7 @@ import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactory import { AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { FileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/fileEditorTracker'; -import { SaveErrorHandler } from 'vs/workbench/contrib/files/browser/saveErrorHandler'; +import { TextFileSaveErrorHandler } from 'vs/workbench/contrib/files/browser/textFileSaveErrorHandler'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { BinaryFileEditor } from 'vs/workbench/contrib/files/browser/editors/binaryFileEditor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -166,8 +166,8 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Register File Editor Tracker Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileEditorTracker, LifecyclePhase.Starting); -// Register Save Error Handler -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SaveErrorHandler, LifecyclePhase.Starting); +// Register Text File Save Error Handler +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TextFileSaveErrorHandler, LifecyclePhase.Starting); // Register uri display for file uris Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileUriLabelContribution, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index d492c77b3b0..6b99f39e922 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -6,20 +6,15 @@ import { URI } from 'vs/base/common/uri'; import { IListService } from 'vs/platform/list/browser/listService'; import { OpenEditor } from 'vs/workbench/contrib/files/common/files'; -import { toResource, SideBySideEditor } from 'vs/workbench/common/editor'; +import { toResource, SideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { coalesce } from 'vs/base/common/arrays'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -// Commands can get exeucted from a command pallete, from a context menu or from some list using a keybinding -// To cover all these cases we need to properly compute the resource on which the command is being executed -export function getResourceForCommand(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): URI | undefined { - if (URI.isUri(resource)) { - return resource; - } - +function getFocus(listService: IListService): unknown | undefined { let list = listService.lastFocusedList; if (list?.getHTMLElement() === document.activeElement) { let focus: unknown; @@ -35,16 +30,40 @@ export function getResourceForCommand(resource: URI | object | undefined, listSe } } - if (focus instanceof ExplorerItem) { - return focus.resource; - } else if (focus instanceof OpenEditor) { - return focus.getResource(); - } + return focus; + } + + return undefined; +} + +// Commands can get exeucted from a command pallete, from a context menu or from some list using a keybinding +// To cover all these cases we need to properly compute the resource on which the command is being executed +export function getResourceForCommand(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): URI | undefined { + if (URI.isUri(resource)) { + return resource; + } + + const focus = getFocus(listService); + if (focus instanceof ExplorerItem) { + return focus.resource; + } else if (focus instanceof OpenEditor) { + return focus.getResource(); } return editorService.activeEditor ? toResource(editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }) : undefined; } +export function getEditorForCommand(listService: IListService, editorGroupService: IEditorGroupsService): IEditorIdentifier | undefined { + const focus = getFocus(listService); + if (focus instanceof OpenEditor) { + return focus; + } + + const activeGroup = editorGroupService.activeGroup; + + return activeGroup.activeEditor ? { groupId: activeGroup.id, editor: activeGroup.activeEditor } : undefined; +} + export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): Array { const list = listService.lastFocusedList; if (list?.getHTMLElement() === document.activeElement) { @@ -83,3 +102,26 @@ export function getMultiSelectedResources(resource: URI | object | undefined, li const result = getResourceForCommand(resource, listService, editorService); return !!result ? [result] : []; } + +export function getMultiSelectedEditors(listService: IListService, editorGroupsService: IEditorGroupsService): Array { + const list = listService.lastFocusedList; + if (list?.getHTMLElement() === document.activeElement) { + // Open editors view + if (list instanceof List) { + const selection = coalesce(list.getSelectedElements().filter(s => s instanceof OpenEditor)); + const focusedElements = list.getFocusedElements(); + const focus = focusedElements.length ? focusedElements[0] : undefined; + let mainEditor: IEditorIdentifier | undefined = undefined; + if (focus instanceof OpenEditor) { + mainEditor = focus; + } + // We only respect the selection if it contains the main element. + if (selection.some(s => s === mainEditor)) { + return selection; + } + } + } + + const result = getEditorForCommand(listService, editorGroupsService); + return !!result ? [result] : []; +} diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 83871571794..40510378cfe 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -128,6 +128,16 @@ line-height: normal; } +.explorer-viewlet .explorer-item .monaco-icon-name-container.multiple > .label-name > .monaco-highlighted-label { + padding: 1px; + border-radius: 3px; +} + +.explorer-viewlet .explorer-item .monaco-icon-name-container.multiple > .label-name:hover > .monaco-highlighted-label, +.explorer-viewlet .monaco-list .monaco-list-row.focused .explorer-item .monaco-icon-name-container.multiple > .label-name.active > .monaco-highlighted-label { + text-decoration: underline; +} + .monaco-workbench.linux .explorer-viewlet .explorer-item .monaco-inputbox, .monaco-workbench.mac .explorer-viewlet .explorer-item .monaco-inputbox { height: 22px; diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts similarity index 98% rename from src/vs/workbench/contrib/files/browser/saveErrorHandler.ts rename to src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts index 635033681e1..f4fdf7261cf 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts @@ -41,8 +41,8 @@ const LEARN_MORE_DIRTY_WRITE_IGNORE_KEY = 'learnMoreDirtyWriteError'; const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content of the file with your changes."); -// A handler for save error happening with conflict resolution actions -export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution { +// A handler for text file save error happening with conflict resolution actions +export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution { private messages: ResourceMap; private conflictResolutionContext: IContextKey; private activeConflictResolutionResource?: URI; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 2b4ff7f2578..188f4eb91b4 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { IAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { memoize } from 'vs/base/common/decorators'; -import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut, ExplorerResourceMoveableToTrash } from 'vs/workbench/contrib/files/common/files'; +import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, ExplorerRootContext, ExplorerResourceReadonlyContext, IExplorerService, ExplorerResourceCut, ExplorerResourceMoveableToTrash, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext } from 'vs/workbench/contrib/files/common/files'; import { NewFolderAction, NewFileAction, FileCopiedContext, RefreshExplorerView, CollapseExplorerView } from 'vs/workbench/contrib/files/browser/fileActions'; import { toResource, SideBySideEditor } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -29,7 +29,7 @@ import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; +import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, ICompressedNavigationController, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; @@ -49,8 +49,19 @@ import { values } from 'vs/base/common/map'; import { first } from 'vs/base/common/arrays'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; -import { dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; +import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; +import { ColorValue, listDropBackground } from 'vs/platform/theme/common/colorRegistry'; +import { Color } from 'vs/base/common/color'; + +interface IExplorerViewColors extends IColorMapping { + listDropBackground?: ColorValue | undefined; +} + +interface IExplorerViewStyles { + listDropBackground?: Color; +} export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; @@ -65,6 +76,14 @@ export class ExplorerView extends ViewletPanel { private rootContext: IContextKey; private resourceMoveableToTrash: IContextKey; + private renderer!: FilesRenderer; + + private styleElement!: HTMLStyleElement; + private compressedFocusContext: IContextKey; + private compressedFocusFirstContext: IContextKey; + private compressedFocusLastContext: IContextKey; + private compressedNavigationController: ICompressedNavigationController | undefined; + // Refresh is needed on the initial explorer open private shouldRefresh = true; private dragHandler!: DelayedDragHandler; @@ -96,15 +115,20 @@ export class ExplorerView extends ViewletPanel { this.resourceContext = instantiationService.createInstance(ResourceContextKey); this._register(this.resourceContext); + this.folderContext = ExplorerFolderContext.bindTo(contextKeyService); this.readonlyContext = ExplorerResourceReadonlyContext.bindTo(contextKeyService); this.rootContext = ExplorerRootContext.bindTo(contextKeyService); this.resourceMoveableToTrash = ExplorerResourceMoveableToTrash.bindTo(contextKeyService); + this.compressedFocusContext = ExplorerCompressedFocusContext.bindTo(contextKeyService); + this.compressedFocusFirstContext = ExplorerCompressedFirstFocusContext.bindTo(contextKeyService); + this.compressedFocusLastContext = ExplorerCompressedLastFocusContext.bindTo(contextKeyService); + + this.explorerService.registerContextProvider(this); const decorationProvider = new ExplorerDecorationsProvider(this.explorerService, contextService); this._register(decorationService.registerDecorationsProvider(decorationProvider)); this._register(decorationProvider); - this._register(this.resourceContext); } get name(): string { @@ -161,6 +185,10 @@ export class ExplorerView extends ViewletPanel { renderBody(container: HTMLElement): void { const treeContainer = DOM.append(container, DOM.$('.explorer-folders-view')); + + this.styleElement = DOM.createStyleSheet(treeContainer); + attachStyler(this.themeService, { listDropBackground }, this.styleListDropBackground.bind(this)); + this.createTree(treeContainer); if (this.toolbar) { @@ -254,6 +282,39 @@ export class ExplorerView extends ViewletPanel { } } + getContext(respectMultiSelection: boolean): ExplorerItem[] { + let focusedStat: ExplorerItem | undefined; + + if (this.compressedNavigationController) { + focusedStat = this.compressedNavigationController.current; + } else { + const focus = this.tree.getFocus(); + focusedStat = focus.length ? focus[0] : undefined; + } + + if (!focusedStat) { + return []; + } + + const selectedStats: ExplorerItem[] = []; + + for (const stat of this.tree.getSelection()) { + const controller = this.renderer.getCompressedNavigationController(stat); + + if (controller) { + selectedStats.push(...controller.items); + } else { + selectedStats.push(stat); + } + } + + if (respectMultiSelection && selectedStats.indexOf(focusedStat) >= 0) { + return selectedStats; + } + + return [focusedStat]; + } + private selectActiveFile(deselect?: boolean, reveal = this.autoReveal): void { if (this.autoReveal) { const activeFile = this.getActiveFile(); @@ -278,14 +339,14 @@ export class ExplorerView extends ViewletPanel { this._register(explorerLabels); const updateWidth = (stat: ExplorerItem) => this.tree.updateWidth(stat); - const filesRenderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels, updateWidth); - this._register(filesRenderer); + this.renderer = this.instantiationService.createInstance(FilesRenderer, explorerLabels, updateWidth); + this._register(this.renderer); this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); const isCompressionEnabled = () => this.configurationService.getValue('explorer.compressSingleChildFolders'); - this.tree = this.instantiationService.createInstance>(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [filesRenderer], + this.tree = this.instantiationService.createInstance>(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer], this.instantiationService.createInstance(ExplorerDataSource), { compressionEnabled: isCompressionEnabled(), accessibilityProvider: new ExplorerAccessibilityProvider(), @@ -391,7 +452,7 @@ export class ExplorerView extends ViewletPanel { } } - private setContextKeys(stat: ExplorerItem | null): void { + private setContextKeys(stat: ExplorerItem | null | undefined): void { const isSingleFolder = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER; const resource = stat ? stat.resource : isSingleFolder ? this.contextService.getWorkspace().folders[0].uri : null; this.resourceContext.set(resource); @@ -401,7 +462,18 @@ export class ExplorerView extends ViewletPanel { } private onContextMenu(e: ITreeContextMenuEvent): void { - const stat = e.element; + const disposables = new DisposableStore(); + let stat = e.element; + let anchor = e.anchor; + + // Compressed folders + if (stat) { + const controller = this.renderer.getCompressedNavigationController(stat); + + if (controller) { + anchor = controller.labels[controller.index]; + } + } // update dynamic contexts this.fileCopiedContextKey.set(this.clipboardService.hasResources()); @@ -412,17 +484,17 @@ export class ExplorerView extends ViewletPanel { const actions: IAction[] = []; const roots = this.explorerService.roots; // If the click is outside of the elements pass the root resource if there is only one root. If there are multiple roots pass empty object. const arg = stat instanceof ExplorerItem ? stat.resource : roots.length === 1 ? roots[0].resource : {}; - const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { arg, shouldForwardArgs: true }, actions, this.contextMenuService); + disposables.add(createAndFillInContextMenuActions(this.contributedContextMenu, { arg, shouldForwardArgs: true }, actions, this.contextMenuService)); this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, + getAnchor: () => anchor, getActions: () => actions, onHide: (wasCancelled?: boolean) => { if (wasCancelled) { this.tree.domFocus(); } - dispose(actionsDisposable); + disposables.dispose(); }, getActionsContext: () => stat && selection && selection.indexOf(stat) >= 0 ? selection.map((fs: ExplorerItem) => fs.resource) @@ -431,7 +503,7 @@ export class ExplorerView extends ViewletPanel { } private onFocusChanged(elements: ExplorerItem[]): void { - const stat = elements && elements.length ? elements[0] : null; + const stat = elements && elements.length ? elements[0] : undefined; this.setContextKeys(stat); if (stat) { @@ -441,6 +513,17 @@ export class ExplorerView extends ViewletPanel { } else { this.resourceMoveableToTrash.reset(); } + + this.compressedNavigationController = stat && this.renderer.getCompressedNavigationController(stat); + + if (!this.compressedNavigationController) { + this.compressedFocusContext.set(false); + return; + } + + this.compressedFocusContext.set(true); + // this.compressedNavigationController.last(); + this.updateCompressedNavigationContextKeys(this.compressedNavigationController); } // General methods @@ -589,6 +672,60 @@ export class ExplorerView extends ViewletPanel { this.tree.collapseAll(); } + previousCompressedStat(): void { + if (!this.compressedNavigationController) { + return; + } + + this.compressedNavigationController.previous(); + this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + } + + nextCompressedStat(): void { + if (!this.compressedNavigationController) { + return; + } + + this.compressedNavigationController.next(); + this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + } + + firstCompressedStat(): void { + if (!this.compressedNavigationController) { + return; + } + + this.compressedNavigationController.first(); + this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + } + + lastCompressedStat(): void { + if (!this.compressedNavigationController) { + return; + } + + this.compressedNavigationController.last(); + this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + } + + private updateCompressedNavigationContextKeys(controller: ICompressedNavigationController): void { + this.compressedFocusFirstContext.set(controller.index === 0); + this.compressedFocusLastContext.set(controller.index === controller.count - 1); + } + + styleListDropBackground(styles: IExplorerViewStyles): void { + const content: string[] = []; + + if (styles.listDropBackground) { + content.push(`.explorer-viewlet .explorer-item .monaco-icon-name-container.multiple > .label-name.drop-target > .monaco-highlighted-label { background-color: ${styles.listDropBackground}; }`); + } + + const newStyles = content.join('\n'); + if (newStyles !== this.styleElement.innerHTML) { + this.styleElement.innerHTML = newStyles; + } + } + dispose(): void { if (this.dragHandler) { this.dragHandler.dispose(); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index f88860ab91b..733c710903f 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -12,7 +12,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; @@ -51,6 +51,9 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree' import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { VSBuffer } from 'vs/base/common/buffer'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { isNumber } from 'vs/base/common/types'; +import { domEvent } from 'vs/base/browser/event'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -114,6 +117,77 @@ export class ExplorerDataSource implements IAsyncDataSource= this.items.length - 1) { + return; + } + + this.setIndex(this._index + 1); + } + + first(): void { + if (this._index === 0) { + return; + } + + this.setIndex(0); + } + + last(): void { + if (this._index === this.items.length - 1) { + return; + } + + this.setIndex(this.items.length - 1); + } + + setIndex(index: number): void { + if (index < 0 || index >= this.items.length) { + return; + } + + DOM.removeClass(this.labels[this._index], 'active'); + this._index = index; + DOM.addClass(this.labels[this._index], 'active'); + } +} + export interface IFileTemplateData { elementDisposable: IDisposable; label: IResourceLabel; @@ -125,6 +199,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); constructor( private labels: ResourceLabels, @@ -132,7 +207,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); this.configListener = this.configurationService.onDidChangeConfiguration(e => { @@ -158,6 +234,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer e.name).join('/'); + const label = node.element.elements.map(e => e.name); const editableData = this.explorerService.getEditableData(stat); // File Label if (!editableData) { + DOM.addClass(templateData.label.element, 'compressed'); templateData.label.element.style.display = 'flex'; - templateData.elementDisposable = this.renderStat(stat, label, node.filterData, templateData); + + const disposables = new DisposableStore(); + disposables.add(this.renderStat(stat, label, node.filterData, templateData)); + + const compressedNavigationController = new CompressedNavigationController(node.element.elements, templateData); + this.compressedNavigationControllers.set(stat, compressedNavigationController); + + domEvent(templateData.container, 'mousedown')(e => { + const result = getIconLabelNameFromHTMLElement(e.target); + + if (result) { + compressedNavigationController.setIndex(result.index); + } + }, undefined, disposables); + + disposables.add(toDisposable(() => { + this.compressedNavigationControllers.delete(stat); + })); + + templateData.elementDisposable = disposables; } // Input Box else { + DOM.removeClass(templateData.label.element, 'compressed'); templateData.label.element.style.display = 'none'; templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData); } } - private renderStat(stat: ExplorerItem, label: string, filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable { + private renderStat(stat: ExplorerItem, label: string | string[], filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable { templateData.label.element.style.display = 'flex'; const extraClasses = ['explorer-item']; if (this.explorerService.isCut(stat)) { @@ -202,7 +301,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer { @@ -227,6 +327,9 @@ export class FilesRenderer implements ICompressibleTreeRenderer, index: number, templateData: IFileTemplateData): void { + disposeElement(element: ITreeNode, index: number, templateData: IFileTemplateData): void { + templateData.elementDisposable.dispose(); + } + + disposeCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IFileTemplateData): void { templateData.elementDisposable.dispose(); } @@ -299,6 +406,10 @@ export class FilesRenderer implements ICompressibleTreeRenderer { export class FileDragAndDrop implements ITreeDragAndDrop { private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop'; + private compressedDragOverElement: HTMLElement | undefined; + private compressedDropTargetDisposable: IDisposable = Disposable.None; + private toDispose: IDisposable[]; private dropEnabled = false; @@ -498,6 +612,42 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return false; } + // Compressed folders + if (target) { + const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent); + + if (compressedTarget) { + const iconLabelName = getIconLabelNameFromHTMLElement(originalEvent.target); + + if (iconLabelName && iconLabelName.index < iconLabelName.count - 1) { + const result = this._onDragOver(data, compressedTarget, targetIndex, originalEvent); + + if (result) { + if (iconLabelName.element !== this.compressedDragOverElement) { + this.compressedDragOverElement = iconLabelName.element; + this.compressedDropTargetDisposable.dispose(); + this.compressedDropTargetDisposable = toDisposable(() => { + DOM.removeClass(iconLabelName.element, 'drop-target'); + this.compressedDragOverElement = undefined; + }); + + DOM.addClass(iconLabelName.element, 'drop-target'); + } + + return typeof result === 'boolean' ? result : { ...result, feedback: [] }; + } + + this.compressedDropTargetDisposable.dispose(); + return false; + } + } + } + + this.compressedDropTargetDisposable.dispose(); + return this._onDragOver(data, target, targetIndex, originalEvent); + } + + private _onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction { const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh)); const fromDesktop = data instanceof DesktopDragAndDropData; const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move; @@ -516,7 +666,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // In-Explorer DND else { - const items = (data as ElementsDragAndDropData).elements; + const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData); if (!target) { // Dropping onto the empty area. Do not accept if items dragged are already @@ -591,16 +741,17 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return element.resource.toString(); } - getDragLabel(elements: ExplorerItem[]): string | undefined { - if (elements.length > 1) { - return String(elements.length); + getDragLabel(elements: ExplorerItem[], originalEvent: DragEvent): string | undefined { + if (elements.length === 1) { + const stat = FileDragAndDrop.getCompressedStatFromDragEvent(elements[0], originalEvent); + return stat.name; } - return elements[0].name; + return String(elements.length); } onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { - const items = (data as ElementsDragAndDropData).elements; + const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData, originalEvent); if (items && items.length && originalEvent.dataTransfer) { // Apply some datatransfer types to allow for dragging the element outside of the application this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent); @@ -615,6 +766,17 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void { + this.compressedDropTargetDisposable.dispose(); + + // Find compressed target + if (target) { + const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent); + + if (compressedTarget) { + target = compressedTarget; + } + } + // Find parent to add to if (!target) { target = this.explorerService.roots[this.explorerService.roots.length - 1]; @@ -628,41 +790,43 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Desktop DND (Import file) if (data instanceof DesktopDragAndDropData) { - this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); + if (isWeb) { + this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); + } else { + this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); + } } // In-Explorer DND (Move/Copy file) else { - this.handleExplorerDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); + this.handleExplorerDrop(data as ElementsDragAndDropData, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); } } - private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { - if (isWeb) { - data.files.forEach(file => { - const reader = new FileReader(); - reader.readAsArrayBuffer(file); - reader.onload = async (event) => { - const name = file.name; - if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) { - if (target.getChild(name)) { - const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm(name)); - if (!confirmed) { - return; - } - } - - const resource = joinPath(target.resource, name); - await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target?.result))); - if (data.files.length === 1) { - await this.editorService.openEditor({ resource, options: { pinned: true } }); + private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + data.files.forEach(file => { + const reader = new FileReader(); + reader.readAsArrayBuffer(file); + reader.onload = async (event) => { + const name = file.name; + if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) { + if (target.getChild(name)) { + const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm(name)); + if (!confirmed) { + return; } } - }; - }); - return; - } + const resource = joinPath(target.resource, name); + await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target?.result))); + if (data.files.length === 1) { + await this.editorService.openEditor({ resource, options: { pinned: true } }); + } + } + }; + }); + } + private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { const droppedResources = extractResources(originalEvent, true); // Check for dropped external files to be folders const result = await this.fileService.resolveAll(droppedResources); @@ -755,8 +919,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } - private async handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { - const elementsData = (data as ElementsDragAndDropData).elements; + private async handleExplorerDrop(data: ElementsDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data); const items = distinctParents(elementsData, s => s.resource); const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); @@ -869,6 +1033,66 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } } + + private static getStatsFromDragAndDropData(data: ElementsDragAndDropData, dragStartEvent?: DragEvent): ExplorerItem[] { + if (data.context) { + return data.context; + } + + // Detect compressed folder dragging + if (dragStartEvent && data.elements.length === 1) { + data.context = [FileDragAndDrop.getCompressedStatFromDragEvent(data.elements[0], dragStartEvent)]; + return data.context; + } + + return data.elements; + } + + private static getCompressedStatFromDragEvent(stat: ExplorerItem, dragEvent: DragEvent): ExplorerItem { + const target = document.elementFromPoint(dragEvent.clientX, dragEvent.clientY); + const iconLabelName = getIconLabelNameFromHTMLElement(target); + + if (iconLabelName) { + const { count, index } = iconLabelName; + + let i = count - 1; + while (i > index && stat.parent) { + stat = stat.parent; + i--; + } + + return stat; + } + + return stat; + } + + onDragEnd(): void { + this.compressedDropTargetDisposable.dispose(); + } +} + +function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement, count: number, index: number } | null { + if (!(target instanceof HTMLElement)) { + return null; + } + + let element: HTMLElement | null = target; + + while (element && !DOM.hasClass(element, 'monaco-list-row')) { + if (DOM.hasClass(element, 'label-name') && element.hasAttribute('data-icon-label-count')) { + const count = Number(element.getAttribute('data-icon-label-count')); + const index = Number(element.getAttribute('data-icon-label-index')); + + if (isNumber(count) && isNumber(index)) { + return { element: element, count, index }; + } + } + + element = element.parentElement; + } + + return null; } export class ExplorerCompressionDelegate implements ITreeCompressionDelegate { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 8b93f82c51a..f0786144ffe 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -30,7 +30,7 @@ import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; -import { DirtyEditorContext, OpenEditorsGroupContext } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { DirtyEditorContext, OpenEditorsGroupContext, SaveableEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; @@ -61,6 +61,7 @@ export class OpenEditorsView extends ViewletPanel { private resourceContext!: ResourceContextKey; private groupFocusedContext!: IContextKey; private dirtyEditorFocusedContext!: IContextKey; + private saveableEditorFocusedContext!: IContextKey; constructor( options: IViewletViewOptions, @@ -231,16 +232,19 @@ export class OpenEditorsView extends ViewletPanel { this._register(this.resourceContext); this.groupFocusedContext = OpenEditorsGroupContext.bindTo(this.contextKeyService); this.dirtyEditorFocusedContext = DirtyEditorContext.bindTo(this.contextKeyService); + this.saveableEditorFocusedContext = SaveableEditorContext.bindTo(this.contextKeyService); this._register(this.list.onContextMenu(e => this.onListContextMenu(e))); this.list.onFocusChange(e => { this.resourceContext.reset(); this.groupFocusedContext.reset(); this.dirtyEditorFocusedContext.reset(); + this.saveableEditorFocusedContext.reset(); const element = e.elements.length ? e.elements[0] : undefined; if (element instanceof OpenEditor) { const resource = element.getResource(); this.dirtyEditorFocusedContext.set(element.editor.isDirty()); + this.saveableEditorFocusedContext.set(!element.editor.isReadonly()); this.resourceContext.set(withUndefinedAsNull(resource)); } else if (!!element) { this.groupFocusedContext.set(true); @@ -407,7 +411,7 @@ export class OpenEditorsView extends ViewletPanel { } private updateDirtyIndicator(): void { - let dirty = this.dirtyCount; + let dirty = this.workingCopyService.dirtyCount; if (dirty === 0) { dom.addClass(this.dirtyCountElement, 'hidden'); } else { @@ -416,18 +420,6 @@ export class OpenEditorsView extends ViewletPanel { } } - private get dirtyCount(): number { - let dirtyCount = 0; - - for (const element of this.elements) { - if (element instanceof OpenEditor && element.editor.isDirty()) { - dirtyCount++; - } - } - - return dirtyCount; - } - private get elementCount(): number { return this.editorGroupService.groups.map(g => g.count) .reduce((first, second) => first + second, this.showGroups ? this.editorGroupService.groups.length : 0); diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index 104bebe8fb3..553d195d02c 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -7,10 +7,10 @@ import { localize } from 'vs/nls'; import { createMemoizer } from 'vs/base/common/decorators'; import { dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { EncodingMode, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity, IRevertOptions } from 'vs/workbench/common/editor'; +import { EncodingMode, IFileEditorInput, ITextEditorModel, Verbosity, TextEditorInput, IRevertOptions } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationError, FileOperationResult, IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { ITextFileService, ModelState, TextFileModelChangeEvent, LoadReason, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IReference } from 'vs/base/common/lifecycle'; @@ -18,6 +18,8 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FILE_EDITOR_INPUT_ID, TEXT_FILE_EDITOR_ID, BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const enum ForceOpenAs { None, @@ -28,7 +30,7 @@ const enum ForceOpenAs { /** * A file editor input is the input type for the file editor of file system resources. */ -export class FileEditorInput extends EditorInput implements IFileEditorInput { +export class FileEditorInput extends TextEditorInput implements IFileEditorInput { private static readonly MEMOIZER = createMemoizer(); @@ -39,21 +41,20 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { private textModelReference: Promise> | null = null; - /** - * An editor input who's contents are retrieved from file services. - */ constructor( - private resource: URI, + resource: URI, preferredEncoding: string | undefined, preferredMode: string | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextFileService private readonly textFileService: ITextFileService, + @ITextFileService textFileService: ITextFileService, @ITextModelService private readonly textModelResolverService: ITextModelService, @ILabelService private readonly labelService: ILabelService, @IFileService private readonly fileService: IFileService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IEditorService editorService: IEditorService, + @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(); + super(resource, editorService, editorGroupService, textFileService); if (preferredEncoding) { this.setPreferredEncoding(preferredEncoding); @@ -91,10 +92,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } } - getResource(): URI { - return this.resource; - } - getEncoding(): string | undefined { const textModel = this.textFileService.models.get(this.resource); if (textModel) { @@ -226,6 +223,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return label; } + isReadonly(): boolean { + return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + } + isDirty(): boolean { const model = this.textFileService.models.get(this.resource); if (!model) { @@ -243,10 +244,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return model.isDirty(); } - save(): Promise { - return this.textFileService.save(this.resource); - } - revert(options?: IRevertOptions): Promise { return this.textFileService.revert(this.resource, options); } diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index 75df6d71f72..f9afb180963 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -6,7 +6,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExplorerService, IEditableData, IFilesConfiguration, SortOrder, SortOrderConfiguration } from 'vs/workbench/contrib/files/common/files'; +import { IExplorerService, IEditableData, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files'; import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; @@ -41,6 +41,7 @@ export class ExplorerService implements IExplorerService { private _sortOrder: SortOrder; private cutItems: ExplorerItem[] | undefined; private fileSystemProviderSchemes = new Set(); + private contextProvider: IContextProvider | undefined; constructor( @IFileService private fileService: IFileService, @@ -48,7 +49,7 @@ export class ExplorerService implements IExplorerService { @IConfigurationService private configurationService: IConfigurationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IClipboardService private clipboardService: IClipboardService, - @IEditorService private editorService: IEditorService + @IEditorService private editorService: IEditorService, ) { this._sortOrder = this.configurationService.getValue('explorer.sortOrder'); } @@ -81,6 +82,18 @@ export class ExplorerService implements IExplorerService { return this._sortOrder; } + registerContextProvider(contextProvider: IContextProvider): void { + this.contextProvider = contextProvider; + } + + getContext(respectMultiSelection: boolean): ExplorerItem[] { + if (!this.contextProvider) { + return []; + } + + return this.contextProvider.getContext(respectMultiSelection); + } + // Memoized locals @memoize private get fileEventsFilter(): ResourceGlobMatcher { const fileEventsFilter = this.instantiationService.createInstance( diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index d4e85e31ccf..5eb63e01d92 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -51,6 +51,7 @@ export interface IExplorerService { readonly onDidSelectResource: Event<{ resource?: URI, reveal?: boolean }>; readonly onDidCopyItems: Event<{ items: ExplorerItem[], cut: boolean, previouslyCutItems: ExplorerItem[] | undefined }>; + getContext(respectMultiSelection: boolean): ExplorerItem[]; setEditable(stat: ExplorerItem, data: IEditableData | null): void; getEditable(): { stat: ExplorerItem, data: IEditableData } | undefined; getEditableData(stat: ExplorerItem): IEditableData | undefined; @@ -66,7 +67,14 @@ export interface IExplorerService { * Will try to resolve the path in case the explorer is not yet expanded to the file yet. */ select(resource: URI, reveal?: boolean): Promise; + + registerContextProvider(contextProvider: IContextProvider): void; } + +export interface IContextProvider { + getContext(respectMultiSelection: boolean): ExplorerItem[]; +} + export const IExplorerService = createDecorator('explorerService'); /** @@ -84,6 +92,11 @@ export const OpenEditorsVisibleContext = new RawContextKey('openEditors export const OpenEditorsFocusedContext = new RawContextKey('openEditorsFocus', true); export const ExplorerFocusedContext = new RawContextKey('explorerViewletFocus', true); +// compressed nodes +export const ExplorerCompressedFocusContext = new RawContextKey('explorerViewletCompressedFocus', true); +export const ExplorerCompressedFirstFocusContext = new RawContextKey('explorerViewletCompressedFirstFocus', true); +export const ExplorerCompressedLastFocusContext = new RawContextKey('explorerViewletCompressedLastFocus', true); + export const FilesExplorerFocusCondition = ContextKeyExpr.and(ExplorerViewletVisibleContext, FilesExplorerFocusedContext, ContextKeyExpr.not(InputFocusedContextKey)); export const ExplorerFocusCondition = ContextKeyExpr.and(ExplorerViewletVisibleContext, ExplorerFocusedContext, ContextKeyExpr.not(InputFocusedContextKey)); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 674d1f2a1e4..35e56e70a5f 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -141,7 +141,7 @@ suite('Files - FileEditorInput', () => { resolved.textEditorModel!.setValue('changed'); assert.ok(input.isDirty()); - await input.save(); + await input.save(0); assert.ok(!input.isDirty()); resolved.dispose(); }); diff --git a/src/vs/workbench/contrib/markers/browser/markersPanel.ts b/src/vs/workbench/contrib/markers/browser/markersPanel.ts index 5b650202a90..239f59aa6d8 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersPanel.ts @@ -482,9 +482,8 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { this.setCurrentActiveEditor(); if (this.filterAction.activeFile) { this.refreshPanel(); - } else { - this.autoReveal(); } + this.autoReveal(); } private setCurrentActiveEditor(): void { @@ -547,32 +546,32 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } private autoReveal(focus: boolean = false): void { + // No need to auto reveal if active file filter is on + if (this.filterAction.activeFile) { + return; + } let autoReveal = this.configurationService.getValue('problems.autoReveal'); if (typeof autoReveal === 'boolean' && autoReveal) { - this.revealMarkersForCurrentActiveEditor(focus); - } - } + let currentActiveResource = this.getResourceForCurrentActiveResource(); + if (currentActiveResource) { + if (!this.tree.isCollapsed(currentActiveResource) && this.hasSelectedMarkerFor(currentActiveResource)) { + this.tree.reveal(this.tree.getSelection()[0], this.lastSelectedRelativeTop); + if (focus) { + this.tree.setFocus(this.tree.getSelection()); + } + } else { + this.tree.expand(currentActiveResource); + this.tree.reveal(currentActiveResource, 0); - private revealMarkersForCurrentActiveEditor(focus: boolean = false): void { - let currentActiveResource = this.getResourceForCurrentActiveResource(); - if (currentActiveResource) { - if (!this.tree.isCollapsed(currentActiveResource) && this.hasSelectedMarkerFor(currentActiveResource)) { - this.tree.reveal(this.tree.getSelection()[0], this.lastSelectedRelativeTop); - if (focus) { - this.tree.setFocus(this.tree.getSelection()); - } - } else { - this.tree.expand(currentActiveResource); - this.tree.reveal(currentActiveResource, 0); - - if (focus) { - this.tree.setFocus([currentActiveResource]); - this.tree.setSelection([currentActiveResource]); + if (focus) { + this.tree.setFocus([currentActiveResource]); + this.tree.setSelection([currentActiveResource]); + } } + } else if (focus) { + this.tree.setSelection([]); + this.tree.focusFirst(); } - } else if (focus) { - this.tree.setSelection([]); - this.tree.focusFirst(); } } diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index 963bf4a95ef..73aab9391ec 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -154,10 +154,6 @@ justify-content: center; } -.markers-panel .monaco-tl-contents .actions .action-item { - margin-right: 2px; -} - .markers-panel .markers-panel-container .tree-container .monaco-tl-contents .marker-source, .markers-panel .markers-panel-container .tree-container .monaco-tl-contents .related-info-resource, .markers-panel .markers-panel-container .tree-container .monaco-tl-contents .related-info-resource-separator, diff --git a/src/vs/workbench/contrib/outline/browser/outlineNavigation.ts b/src/vs/workbench/contrib/outline/browser/outlineNavigation.ts index 7d22b68aa69..d7023790f93 100644 --- a/src/vs/workbench/contrib/outline/browser/outlineNavigation.ts +++ b/src/vs/workbench/contrib/outline/browser/outlineNavigation.ts @@ -147,7 +147,7 @@ export class OutlineNavigation implements IEditorContribution { if (modelNow === this._editor.getModel()) { this._editor.deltaDecorations(ids, []); } - }, 250); + }, 350); } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index 144a775d045..77284a59676 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -255,7 +255,7 @@ export class PreferencesEditor extends BaseEditor { if (this.editorService.activeControl !== this) { this.focus(); } - const promise: Promise = this.input && this.input.isDirty() ? this.input.save() : Promise.resolve(true); + const promise: Promise = this.input && this.input.isDirty() ? this.input.save(this.group!.id) : Promise.resolve(true); promise.then(() => { if (target === ConfigurationTarget.USER_LOCAL) { this.preferencesService.switchSettings(ConfigurationTarget.USER_LOCAL, this.preferencesService.userSettingsResource, true); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 329d660bca6..a722df7c506 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -28,7 +28,6 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ISpliceable } from 'vs/base/common/sequence'; import { escapeRegExpCharacters, startsWith } from 'vs/base/common/strings'; -import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -47,6 +46,8 @@ import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/ import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { isArray } from 'vs/base/common/types'; +import { BrowserFeatures } from 'vs/base/browser/canIUse'; +import { isIOS } from 'vs/base/common/platform'; const $ = DOM.$; @@ -485,15 +486,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre }; this._onDidClickSettingLink.fire(e); } else { - let uri: URI | undefined; - try { - uri = URI.parse(content); - } catch (err) { - // ignore - } - if (uri) { - this._openerService.open(uri).catch(onUnexpectedError); - } + this._openerService.open(content).catch(onUnexpectedError); } }, disposeables @@ -918,7 +911,9 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre renderTemplate(container: HTMLElement): ISettingEnumItemTemplate { const common = this.renderCommonTemplate(null, container, 'enum'); - const selectBox = new SelectBox([], 0, this._contextViewService, undefined, { useCustomDrawn: true }); + const selectBox = new SelectBox([], 0, this._contextViewService, undefined, { + useCustomDrawn: !(isIOS && BrowserFeatures.pointerEvents) + }); common.toDispose.push(selectBox); common.toDispose.push(attachSelectBoxStyler(selectBox, this._themeService, { diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index df72ab685b9..0d5b50909f6 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -135,6 +135,7 @@ .scm-viewlet .monaco-list .monaco-list-row .resource-group > .actions, .scm-viewlet .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { display: none; + max-width: fit-content; } .scm-viewlet .monaco-list .monaco-list-row:hover .resource-group > .actions, diff --git a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts index 7e511a8449d..15a23398c98 100644 --- a/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts +++ b/src/vs/workbench/contrib/splash/electron-browser/partsSplash.contribution.ts @@ -60,10 +60,12 @@ class PartsSplash { if (e.affectsConfiguration('window.titleBarStyle')) { this._didChangeTitleBarStyle = true; this._savePartsSplash(); - } else if (e.affectsConfiguration('workbench.colorTheme') || e.affectsConfiguration('workbench.colorCustomizations')) { - this._savePartsSplash(); } }, this, this._disposables); + + _themeService.onThemeChange(_ => { + this._savePartsSplash(); + }, this, this._disposables); } dispose(): void { diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index d64d2c4ce38..987d0ad21a6 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -1234,7 +1234,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private executeTask(task: Task, resolver: ITaskResolver): Promise { return ProblemMatcherRegistry.onReady().then(() => { - return this.textFileService.saveAll().then((value) => { // make sure all dirty files are saved + return this.editorService.saveAll().then((value) => { // make sure all dirty editors are saved let executeResult = this.getTaskSystem().run(task, resolver); return this.handleExecuteResult(executeResult); }); @@ -2164,7 +2164,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } ProblemMatcherRegistry.onReady().then(() => { - return this.textFileService.saveAll().then((value) => { // make sure all dirty files are saved + return this.editorService.saveAll().then((value) => { // make sure all dirty editors are saved let executeResult = this.getTaskSystem().rerun(); if (executeResult) { return this.handleExecuteResult(executeResult); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index f166e7aacdd..19da528046f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -209,11 +209,12 @@ configurationRegistry.registerConfiguration({ }, 'terminal.integrated.rendererType': { type: 'string', - enum: ['auto', 'canvas', 'dom'], + enum: ['auto', 'canvas', 'dom', 'experimentalWebgl'], enumDescriptions: [ nls.localize('terminal.integrated.rendererType.auto', "Let VS Code guess which renderer to use."), - nls.localize('terminal.integrated.rendererType.canvas', "Use the standard GPU/canvas-based renderer"), - nls.localize('terminal.integrated.rendererType.dom', "Use the fallback DOM-based renderer.") + nls.localize('terminal.integrated.rendererType.canvas', "Use the standard GPU/canvas-based renderer."), + nls.localize('terminal.integrated.rendererType.dom', "Use the fallback DOM-based renderer."), + nls.localize('terminal.integrated.rendererType.experimentalWebgl', "Use the experimental webgl-based renderer. Note that this has some [known issues](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) and this will only be enabled for new terminals (not hot swappable like the other renderers).") ], default: 'auto', description: nls.localize('terminal.integrated.rendererType', "Controls how the terminal is rendered.") @@ -552,20 +553,21 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FindPrevious, mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3, KeyCode.Enter] }, }, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED), 'Terminal: Find previous', category); -// Commands miht be affected by Web restrictons +// Commands might be affected by Web restrictons if (BrowserFeatures.clipboard.writeText) { actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + win: { primary: KeyCode.Ctrl | KeyCode.KEY_C, secondary: [KeyCode.Ctrl | KeyCode.Shift | KeyCode.KEY_C] }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C } }, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FOCUS)), 'Terminal: Copy Selection', category); } if (BrowserFeatures.clipboard.readText) { actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(TerminalPasteAction, TerminalPasteAction.ID, TerminalPasteAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, + win: { primary: KeyCode.Ctrl | KeyCode.KEY_V, secondary: [KeyCode.Ctrl | KeyCode.Shift | KeyCode.KEY_V] }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Paste into Active Terminal', category); } - (new SendSequenceTerminalCommand({ id: SendSequenceTerminalCommand.ID, precondition: undefined, diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 6b0661692ad..1591ec7f6f7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -6,6 +6,7 @@ import { Terminal as XTermTerminal } from 'xterm'; import { WebLinksAddon as XTermWebLinksAddon } from 'xterm-addon-web-links'; import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions } from 'vs/workbench/contrib/terminal/common/terminal'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment, Platform } from 'vs/base/common/platform'; @@ -31,6 +32,7 @@ export interface ITerminalInstanceService { getXtermConstructor(): Promise; getXtermWebLinksConstructor(): Promise; getXtermSearchConstructor(): Promise; + getXtermWebglConstructor(): Promise; createWindowsShellHelper(shellProcessId: number, instance: ITerminalInstance, xterm: XTermTerminal): IWindowsShellHelper; createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): ITerminalChildProcess; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 2971096e09d..45973bb6d7b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -305,17 +305,7 @@ export class CreateNewWithCwdTerminalCommand extends Command { public runCommand(accessor: ServicesAccessor, args: { cwd: string } | undefined): Promise { const terminalService = accessor.get(ITerminalService); - const configurationResolverService = accessor.get(IConfigurationResolverService); - const workspaceContextService = accessor.get(IWorkspaceContextService); - const historyService = accessor.get(IHistoryService); - const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot(Schemas.file); - const lastActiveWorkspaceRoot = activeWorkspaceRootUri ? withNullAsUndefined(workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; - - let cwd: string | undefined; - if (args && args.cwd) { - cwd = configurationResolverService.resolve(lastActiveWorkspaceRoot, args.cwd); - } - const instance = terminalService.createTerminal({ cwd }); + const instance = terminalService.createTerminal({ cwd: args?.cwd }); if (!instance) { return Promise.resolve(undefined); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 3cc02fb30b2..8ddb38770fc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -471,7 +471,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { fastScrollModifier: 'alt', fastScrollSensitivity: editorOptions.fastScrollSensitivity, scrollSensitivity: editorOptions.mouseWheelScrollSensitivity, - rendererType: config.rendererType === 'auto' ? 'canvas' : config.rendererType + rendererType: config.rendererType === 'auto' || config.rendererType === 'experimentalWebgl' ? 'canvas' : config.rendererType }); this._xterm = xterm; this._xtermCore = (xterm as any)._core as XTermCore; @@ -568,6 +568,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._wrapperElement.appendChild(this._xtermElement); this._container.appendChild(this._wrapperElement); xterm.open(this._xtermElement); + if (this._configHelper.config.rendererType === 'experimentalWebgl') { + this._terminalInstanceService.getXtermWebglConstructor().then(Addon => { + xterm.loadAddon(new Addon()); + }); + } if (!xterm.element || !xterm.textarea) { throw new Error('xterm elements not set after open'); @@ -1217,7 +1222,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._safeSetOption('macOptionIsMeta', config.macOptionIsMeta); this._safeSetOption('macOptionClickForcesSelection', config.macOptionClickForcesSelection); this._safeSetOption('rightClickSelectsWord', config.rightClickBehavior === 'selectWord'); - this._safeSetOption('rendererType', config.rendererType === 'auto' ? 'canvas' : config.rendererType); + if (config.rendererType !== 'experimentalWebgl') { + // Never set webgl as it's an addon not a rendererType + this._safeSetOption('rendererType', config.rendererType === 'auto' ? 'canvas' : config.rendererType); + } const editorOptions = this._configurationService.getValue('editor'); this._safeSetOption('fastScrollSensitivity', editorOptions.fastScrollSensitivity); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index fde358b23c0..5fa453db273 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -8,6 +8,7 @@ import { IWindowsShellHelper, ITerminalChildProcess, IDefaultShellAndArgsRequest import { Terminal as XTermTerminal } from 'xterm'; import { WebLinksAddon as XTermWebLinksAddon } from 'xterm-addon-web-links'; import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -15,6 +16,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; let Terminal: typeof XTermTerminal; let WebLinksAddon: typeof XTermWebLinksAddon; let SearchAddon: typeof XTermSearchAddon; +let WebglAddon: typeof XTermWebglAddon; export class TerminalInstanceService implements ITerminalInstanceService { public _serviceBrand: undefined; @@ -43,6 +45,13 @@ export class TerminalInstanceService implements ITerminalInstanceService { return SearchAddon; } + public async getXtermWebglConstructor(): Promise { + if (!WebglAddon) { + WebglAddon = (await import('xterm-addon-webgl')).WebglAddon; + } + return WebglAddon; + } + public createWindowsShellHelper(): IWindowsShellHelper { throw new Error('Not implemented'); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts index 544cabcde0d..6cb08ae0b26 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts @@ -266,8 +266,7 @@ export class TerminalLinkHandler { } private _handleHypertextLink(url: string): void { - const uri = URI.parse(url); - this._openerService.open(uri, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) }); + this._openerService.open(url, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) }); } private _isLinkActivationModifierDown(event: MouseEvent): boolean { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 8527a18f6c4..f04e5f9530f 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -87,7 +87,7 @@ export interface ITerminalConfiguration { }; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; - rendererType: 'auto' | 'canvas' | 'dom'; + rendererType: 'auto' | 'canvas' | 'dom' | 'experimentalWebgl'; rightClickBehavior: 'default' | 'copyPaste' | 'paste' | 'selectWord'; cursorBlinking: boolean; cursorStyle: string; diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index 31b85a9ccdd..cb2aebdc148 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -184,23 +184,16 @@ export function getCwd( logService?: ILogService ): string { if (shell.cwd) { - return (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd; + const unresolved = (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd; + const resolved = _resolveCwd(unresolved, lastActiveWorkspace, configurationResolverService); + return resolved || unresolved; } let cwd: string | undefined; if (!shell.ignoreConfigurationCwd && customCwd) { if (configurationResolverService) { - try { - customCwd = configurationResolverService.resolve(lastActiveWorkspace, customCwd); - } catch (e) { - // There was an issue resolving a variable, log the error in the console and - // fallback to the default. - if (logService) { - logService.error('Could not resolve terminal.integrated.cwd', e); - } - customCwd = undefined; - } + customCwd = _resolveCwd(customCwd, lastActiveWorkspace, configurationResolverService, logService); } if (customCwd) { if (path.isAbsolute(customCwd)) { @@ -219,6 +212,18 @@ export function getCwd( return _sanitizeCwd(cwd); } +function _resolveCwd(cwd: string, lastActiveWorkspace: IWorkspaceFolder | undefined, configurationResolverService: IConfigurationResolverService | undefined, logService?: ILogService): string | undefined { + if (configurationResolverService) { + try { + return configurationResolverService.resolve(lastActiveWorkspace, cwd); + } catch (e) { + logService?.error('Could not resolve terminal cwd', e); + return undefined; + } + } + return cwd; +} + function _sanitizeCwd(cwd: string): string { // Make the drive letter uppercase on Windows (see #9448) if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') { diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts index fd87ba3c734..a825775d8d2 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts @@ -13,6 +13,7 @@ import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; import { Terminal as XTermTerminal } from 'xterm'; import { WebLinksAddon as XTermWebLinksAddon } from 'xterm-addon-web-links'; import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getDefaultShell, getDefaultShellArgs } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; @@ -25,6 +26,7 @@ import { ILogService } from 'vs/platform/log/common/log'; let Terminal: typeof XTermTerminal; let WebLinksAddon: typeof XTermWebLinksAddon; let SearchAddon: typeof XTermSearchAddon; +let WebglAddon: typeof XTermWebglAddon; export class TerminalInstanceService implements ITerminalInstanceService { public _serviceBrand: undefined; @@ -61,6 +63,13 @@ export class TerminalInstanceService implements ITerminalInstanceService { return SearchAddon; } + public async getXtermWebglConstructor(): Promise { + if (!WebglAddon) { + WebglAddon = (await import('xterm-addon-webgl')).WebglAddon; + } + return WebglAddon; + } + public createWindowsShellHelper(shellProcessId: number, instance: ITerminalInstance, xterm: XTermTerminal): IWindowsShellHelper { return new WindowsShellHelper(shellProcessId, instance, xterm); } diff --git a/src/vs/workbench/contrib/terminal/test/electron-browser/terminalLinkHandler.test.ts b/src/vs/workbench/contrib/terminal/test/electron-browser/terminalLinkHandler.test.ts index a48c60f7b4a..a8c87781724 100644 --- a/src/vs/workbench/contrib/terminal/test/electron-browser/terminalLinkHandler.test.ts +++ b/src/vs/workbench/contrib/terminal/test/electron-browser/terminalLinkHandler.test.ts @@ -46,6 +46,9 @@ class MockTerminalInstanceService implements ITerminalInstanceService { getXtermSearchConstructor(): Promise { throw new Error('Method not implemented.'); } + getXtermWebglConstructor(): Promise { + throw new Error('Method not implemented.'); + } createWindowsShellHelper(): any { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts index 1a716bb0180..661b8412cad 100644 --- a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts +++ b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, IRevertOptions, EditorOptions } from 'vs/workbench/common/editor'; +import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, EditorOptions, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom'; @@ -143,6 +143,10 @@ class TestCustomEditorInput extends EditorInput implements IWorkingCopy { setValue(value: string) { if (this.model) { + if (this.model.value === value) { + return; + } + this.model.value = value; } @@ -156,20 +160,30 @@ class TestCustomEditorInput extends EditorInput implements IWorkingCopy { } } + isReadonly(): boolean { + return false; + } + isDirty(): boolean { return this.dirty; } - save(): Promise { + async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { this.setDirty(false); - return Promise.resolve(true); + return true; } - revert(options?: IRevertOptions): Promise { + async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { this.setDirty(false); - return Promise.resolve(true); + return true; + } + + async revert(options?: IRevertOptions): Promise { + this.setDirty(false); + + return true; } async resolve(): Promise { diff --git a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts index f930d68bccf..d8b03dc04d0 100644 --- a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts @@ -5,11 +5,10 @@ import { Schemas } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -21,6 +20,7 @@ import { import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; + export class OpenerValidatorContributions implements IWorkbenchContribution { constructor( @IOpenerService private readonly _openerService: IOpenerService, @@ -34,13 +34,16 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); } - async validateLink(resource: URI): Promise { - const { scheme, authority, path, query, fragment } = resource; - - if (!equalsIgnoreCase(scheme, Schemas.http) && !equalsIgnoreCase(scheme, Schemas.https)) { + async validateLink(resource: URI | string): Promise { + if (!matchesScheme(resource, Schemas.http) && !matchesScheme(resource, Schemas.https)) { return true; } + if (typeof resource === 'string') { + resource = URI.parse(resource); + } + const { scheme, authority, path, query, fragment } = resource; + const domainToOpen = `${scheme}://${authority}`; const { defaultTrustedDomains, trustedDomains } = readTrustedDomains(this._storageService, this._productService); const allTrustedDomains = [...defaultTrustedDomains, ...trustedDomains]; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 5e631d16273..fbac49c504c 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -307,7 +307,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); const signingInCommandId = 'workbench.userData.actions.signingin'; - CommandsRegistry.registerCommand(signInCommandId, () => null); + CommandsRegistry.registerCommand(signingInCommandId, () => null); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', command: { diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index cf633fbc463..c9c0c88a08c 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -19,6 +19,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey('webviewFindWidgetVisible', false); export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED = new RawContextKey('webviewFindWidgetFocused', false); +export const webviewHasOwnEditFunctionsContextKey = 'webviewHasOwnEditFunctions'; +export const webviewHasOwnEditFunctionsContext = new RawContextKey(webviewHasOwnEditFunctionsContextKey, false); + export const IWebviewService = createDecorator('webviewService'); /** diff --git a/src/vs/workbench/contrib/webview/common/resourceLoader.ts b/src/vs/workbench/contrib/webview/common/resourceLoader.ts index 4b7b7cfdc44..e34d1ec28bd 100644 --- a/src/vs/workbench/contrib/webview/common/resourceLoader.ts +++ b/src/vs/workbench/contrib/webview/common/resourceLoader.ts @@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { getWebviewContentMimeType } from 'vs/workbench/contrib/webview/common/mimeTypes'; +import { isUNC } from 'vs/base/common/extpath'; export const WebviewResourceScheme = 'vscode-resource'; @@ -95,6 +96,13 @@ function normalizeRequestPath(requestUri: URI) { } function containsResource(root: URI, resource: URI): boolean { - const rootPath = root.fsPath + (endsWith(root.fsPath, sep) ? '' : sep); + let rootPath = root.fsPath + (endsWith(root.fsPath, sep) ? '' : sep); + let resourceFsPath = resource.fsPath; + + if (isUNC(root.fsPath) && isUNC(resource.fsPath)) { + rootPath = rootPath.toLowerCase(); + resourceFsPath = resourceFsPath.toLowerCase(); + } + return startsWith(resource.fsPath, rootPath); } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts index 1e47c68737b..634dedfcce8 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts @@ -3,17 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { isMacintosh } from 'vs/base/common/platform'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; -import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; import { IWebviewService, webviewDeveloperCategory } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; import * as webviewCommands from 'vs/workbench/contrib/webview/electron-browser/webviewCommands'; import { ElectronWebviewService } from 'vs/workbench/contrib/webview/electron-browser/webviewService'; @@ -27,65 +24,17 @@ actionRegistry.registerWorkbenchAction( webviewDeveloperCategory); function registerWebViewCommands(editorId: string): void { - const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */); + const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */)!; - (new webviewCommands.SelectAllWebviewEditorCommand({ - id: webviewCommands.SelectAllWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_A, - weight: KeybindingWeight.EditorContrib - } - })).register(); + new webviewCommands.SelectAllWebviewEditorCommand(contextKeyExpr).register(); // These commands are only needed on MacOS where we have to disable the menu bar commands if (isMacintosh) { - (new webviewCommands.CopyWebviewEditorCommand({ - id: webviewCommands.CopyWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_C, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - (new webviewCommands.PasteWebviewEditorCommand({ - id: webviewCommands.PasteWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_V, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - (new webviewCommands.CutWebviewEditorCommand({ - id: webviewCommands.CutWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_X, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - (new webviewCommands.UndoWebviewEditorCommand({ - id: webviewCommands.UndoWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, - weight: KeybindingWeight.EditorContrib - } - })).register(); - - (new webviewCommands.RedoWebviewEditorCommand({ - id: webviewCommands.RedoWebviewEditorCommand.ID, - precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, - secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z }, - weight: KeybindingWeight.EditorContrib - } - })).register(); + new webviewCommands.CopyWebviewEditorCommand(contextKeyExpr).register(); + new webviewCommands.PasteWebviewEditorCommand(contextKeyExpr).register(); + new webviewCommands.CutWebviewEditorCommand(contextKeyExpr).register(); + new webviewCommands.UndoWebviewEditorCommand(contextKeyExpr).register(); + new webviewCommands.RedoWebviewEditorCommand(contextKeyExpr).register(); } } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts index 40cc80f75f6..e93e2c38955 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts @@ -3,13 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; -import * as nls from 'vs/nls'; -import { Command, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; -import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewTag } from 'electron'; +import { Action } from 'vs/base/common/actions'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Command, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import * as nls from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { WebviewEditorOverlay, webviewHasOwnEditFunctionsContextKey } from 'vs/workbench/contrib/webview/browser/webview'; import { getActiveWebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewCommands'; +import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; export class OpenWebviewDeveloperToolsAction extends Action { static readonly ID = 'workbench.action.webview.openDeveloperTools'; @@ -36,6 +40,17 @@ export class OpenWebviewDeveloperToolsAction extends Action { export class SelectAllWebviewEditorCommand extends Command { public static readonly ID = 'editor.action.webvieweditor.selectAll'; + constructor(contextKeyExpr: ContextKeyExpr) { + super({ + id: SelectAllWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_A, + weight: KeybindingWeight.EditorContrib + } + }); + } + public runCommand(accessor: ServicesAccessor, args: any): void { withActiveWebviewBasedWebview(accessor, webview => webview.selectAll()); } @@ -44,6 +59,17 @@ export class SelectAllWebviewEditorCommand extends Command { export class CopyWebviewEditorCommand extends Command { public static readonly ID = 'editor.action.webvieweditor.copy'; + constructor(contextKeyExpr: ContextKeyExpr) { + super({ + id: CopyWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + weight: KeybindingWeight.EditorContrib + } + }); + } + public runCommand(accessor: ServicesAccessor, _args: any): void { withActiveWebviewBasedWebview(accessor, webview => webview.copy()); } @@ -52,6 +78,17 @@ export class CopyWebviewEditorCommand extends Command { export class PasteWebviewEditorCommand extends Command { public static readonly ID = 'editor.action.webvieweditor.paste'; + constructor(contextKeyExpr: ContextKeyExpr) { + super({ + id: PasteWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_V, + weight: KeybindingWeight.EditorContrib + } + }); + } + public runCommand(accessor: ServicesAccessor, _args: any): void { withActiveWebviewBasedWebview(accessor, webview => webview.paste()); } @@ -60,6 +97,17 @@ export class PasteWebviewEditorCommand extends Command { export class CutWebviewEditorCommand extends Command { public static readonly ID = 'editor.action.webvieweditor.cut'; + constructor(contextKeyExpr: ContextKeyExpr) { + super({ + id: CutWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_X, + weight: KeybindingWeight.EditorContrib + } + }); + } + public runCommand(accessor: ServicesAccessor, _args: any): void { withActiveWebviewBasedWebview(accessor, webview => webview.cut()); } @@ -68,6 +116,17 @@ export class CutWebviewEditorCommand extends Command { export class UndoWebviewEditorCommand extends Command { public static readonly ID = 'editor.action.webvieweditor.undo'; + constructor(contextKeyExpr: ContextKeyExpr) { + super({ + id: UndoWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey), ContextKeyExpr.not(webviewHasOwnEditFunctionsContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, + weight: KeybindingWeight.EditorContrib + } + }); + } + public runCommand(accessor: ServicesAccessor, args: any): void { withActiveWebviewBasedWebview(accessor, webview => webview.undo()); } @@ -76,6 +135,19 @@ export class UndoWebviewEditorCommand extends Command { export class RedoWebviewEditorCommand extends Command { public static readonly ID = 'editor.action.webvieweditor.redo'; + constructor(contextKeyExpr: ContextKeyExpr) { + super({ + id: RedoWebviewEditorCommand.ID, + precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey), ContextKeyExpr.not(webviewHasOwnEditFunctionsContextKey)), + kbOpts: { + primary: KeyMod.CtrlCmd | KeyCode.KEY_Y, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z }, + weight: KeybindingWeight.EditorContrib + } + }); + } + public runCommand(accessor: ServicesAccessor, args: any): void { withActiveWebviewBasedWebview(accessor, webview => webview.redo()); } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 4d1a4cface3..5e123f78eaa 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -32,9 +32,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ICommandHandler } from 'vs/platform/commands/common/commands'; -import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { toResource } from 'vs/workbench/common/editor'; import { normalizeDriveLetter } from 'vs/base/common/labels'; export namespace OpenLocalFileCommand { @@ -53,13 +51,12 @@ export namespace SaveLocalFileCommand { export const LABEL = nls.localize('saveLocalFile', "Save Local File..."); export function handler(): ICommandHandler { return accessor => { - const textFileService = accessor.get(ITextFileService); const editorService = accessor.get(IEditorService); - let resource: URI | undefined = toResource(editorService.activeEditor); - const options: ISaveOptions = { force: true, availableFileSystems: [Schemas.file] }; - if (resource) { - return textFileService.saveAs(resource, undefined, options); + const activeControl = editorService.activeControl; + if (activeControl) { + return editorService.save({ groupId: activeControl.group.id, editor: activeControl.input }, { saveAs: true, availableFileSystems: [Schemas.file] }); } + return Promise.resolve(undefined); }; } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 7f48cf25e57..c4a20fade35 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -5,7 +5,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput, ITextEditorOptions, IEditorOptions, EditorActivation } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource, SideBySideEditor, IRevertOptions } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -18,8 +18,8 @@ import { URI } from 'vs/base/common/uri'; import { basename, isEqual } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { localize } from 'vs/nls'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection, EditorsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions } 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 } from 'vs/base/common/arrays'; @@ -654,6 +654,93 @@ export class EditorService extends Disposable implements EditorServiceImpl { } //#endregion + + //#region save + + async save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { + + // Convert to array + if (!Array.isArray(editors)) { + editors = [editors]; + } + + // Split editors up into a bucket that is saved in parallel + // and sequentially. Unless "Save As", all non-untitled editors + // can be saved in parallel to speed up the operation. Remaining + // editors are potentially bringing up some UI and thus run + // sequentially. + const editorsToSaveParallel: IEditorIdentifier[] = []; + const editorsToSaveAsSequentially: IEditorIdentifier[] = []; + if (options?.saveAs) { + editorsToSaveAsSequentially.push(...editors); + } else { + for (const { groupId, editor } of editors) { + if (editor.isUntitled()) { + editorsToSaveAsSequentially.push({ groupId, editor }); + } else { + editorsToSaveParallel.push({ groupId, editor }); + } + } + } + + // Editors to save in parallel + await Promise.all(editorsToSaveParallel.map(({ groupId, editor }) => { + + // Use save as a hint to pin the editor + this.editorGroupService.getGroup(groupId)?.pinEditor(editor); + + // Save + return editor.save(groupId, options); + })); + + // Editors to save sequentially + for (const { groupId, editor } of editorsToSaveAsSequentially) { + if (editor.isDisposed()) { + continue; // might have been disposed from from the save already + } + + const result = options?.saveAs ? await editor.saveAs(groupId, options) : await editor.save(groupId, options); + if (!result) { + return false; // failed or cancelled, abort + } + } + + return true; + } + + saveAll(options?: ISaveAllEditorsOptions): Promise { + const editors: IEditorIdentifier[] = []; + + // Collect all editors in MRU order that are dirty + this.forEachDirtyEditor(({ groupId, editor }) => { + if (!editor.isUntitled() || options?.includeUntitled) { + editors.push({ groupId, editor }); + } + }); + + return this.save(editors, options); + } + + async revertAll(options?: IRevertOptions): Promise { + + // Revert each editor in MRU order + const reverts: Promise[] = []; + this.forEachDirtyEditor(({ editor }) => reverts.push(editor.revert(options))); + + await Promise.all(reverts); + } + + private forEachDirtyEditor(callback: (editor: IEditorIdentifier) => void): void { + for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + if (editor.isDirty()) { + callback({ groupId: group.id, editor }); + } + } + } + } + + //#endregion } export interface IEditorOpenHandler { diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index f7708c300e8..3f814693d4a 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -427,11 +427,6 @@ export interface IEditorGroup { */ readonly editors: ReadonlyArray; - /** - * Returns the editor at a specific index of the group. - */ - getEditor(index: number): IEditorInput | undefined; - /** * Get all editors that are currently opened in the group optionally * sorted by being most recent active. Will sort by sequential appearance @@ -439,6 +434,11 @@ export interface IEditorGroup { */ getEditors(order?: EditorsOrder): ReadonlyArray; + /** + * Returns the editor at a specific index of the group. + */ + getEditorByIndex(index: number): IEditorInput | undefined; + /** * Returns the index of the editor in the group or -1 if not opened. */ diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index e6411b1adb8..206aae8ac02 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -5,7 +5,7 @@ import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IEditorIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { Event } from 'vs/base/common/event'; import { IEditor as ICodeEditor } from 'vs/editor/common/editorCommon'; import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -44,6 +44,22 @@ export interface IVisibleEditor extends IEditor { group: IEditorGroup; } +export interface ISaveEditorsOptions extends ISaveOptions { + + /** + * If true, will ask for a location of the editor to save to. + */ + saveAs?: boolean; +} + +export interface ISaveAllEditorsOptions extends ISaveEditorsOptions { + + /** + * Wether to include untitled editors as well. + */ + includeUntitled?: boolean; +} + export interface IEditorService { _serviceBrand: undefined; @@ -184,5 +200,20 @@ export interface IEditorService { /** * Converts a lightweight input to a workbench editor input. */ - createInput(input: IResourceEditor): IEditorInput | null; + createInput(input: IResourceEditor): IEditorInput; + + /** + * Save the provided list of editors. + */ + save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise; + + /** + * Save all editors. + */ + saveAll(options?: ISaveAllEditorsOptions): Promise; + + /** + * Reverts all editors. + */ + revertAll(options?: IRevertOptions): Promise; } diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index ca86ecb8744..05efe8dad59 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -440,8 +440,8 @@ suite('EditorGroupsService', () => { assert.equal(editorWillOpenCounter, 2); assert.equal(editorDidOpenCounter, 2); assert.equal(activeEditorChangeCounter, 1); - assert.equal(group.getEditor(0), input); - assert.equal(group.getEditor(1), inputInactive); + assert.equal(group.getEditorByIndex(0), input); + assert.equal(group.getEditorByIndex(1), inputInactive); assert.equal(group.getIndexOfEditor(input), 0); assert.equal(group.getIndexOfEditor(inputInactive), 1); @@ -491,8 +491,8 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); - assert.equal(group.getEditor(0), input); - assert.equal(group.getEditor(1), inputInactive); + assert.equal(group.getEditorByIndex(0), input); + assert.equal(group.getEditorByIndex(1), inputInactive); await group.closeEditors([input, inputInactive]); assert.equal(group.isEmpty, true); @@ -510,13 +510,13 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); assert.equal(group.count, 3); - assert.equal(group.getEditor(0), input1); - assert.equal(group.getEditor(1), input2); - assert.equal(group.getEditor(2), input3); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); await group.closeEditors({ except: input2 }); assert.equal(group.count, 1); - assert.equal(group.getEditor(0), input2); + assert.equal(group.getEditorByIndex(0), input2); part.dispose(); }); @@ -531,9 +531,9 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); assert.equal(group.count, 3); - assert.equal(group.getEditor(0), input1); - assert.equal(group.getEditor(1), input2); - assert.equal(group.getEditor(2), input3); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); await group.closeEditors({ savedOnly: true }); assert.equal(group.count, 0); @@ -551,14 +551,14 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); assert.equal(group.count, 3); - assert.equal(group.getEditor(0), input1); - assert.equal(group.getEditor(1), input2); - assert.equal(group.getEditor(2), input3); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); await group.closeEditors({ direction: CloseDirection.RIGHT, except: input2 }); assert.equal(group.count, 2); - assert.equal(group.getEditor(0), input1); - assert.equal(group.getEditor(1), input2); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); part.dispose(); }); @@ -573,14 +573,14 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); assert.equal(group.count, 3); - assert.equal(group.getEditor(0), input1); - assert.equal(group.getEditor(1), input2); - assert.equal(group.getEditor(2), input3); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); await group.closeEditors({ direction: CloseDirection.LEFT, except: input2 }); assert.equal(group.count, 2); - assert.equal(group.getEditor(0), input2); - assert.equal(group.getEditor(1), input3); + assert.equal(group.getEditorByIndex(0), input2); + assert.equal(group.getEditorByIndex(1), input3); part.dispose(); }); @@ -594,8 +594,8 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); - assert.equal(group.getEditor(0), input); - assert.equal(group.getEditor(1), inputInactive); + assert.equal(group.getEditorByIndex(0), input); + assert.equal(group.getEditorByIndex(1), inputInactive); await group.closeAllEditors(); assert.equal(group.isEmpty, true); @@ -620,12 +620,12 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); - assert.equal(group.getEditor(0), input); - assert.equal(group.getEditor(1), inputInactive); + assert.equal(group.getEditorByIndex(0), input); + assert.equal(group.getEditorByIndex(1), inputInactive); group.moveEditor(inputInactive, group, { index: 0 }); assert.equal(editorMoveCounter, 1); - assert.equal(group.getEditor(0), inputInactive); - assert.equal(group.getEditor(1), input); + assert.equal(group.getEditorByIndex(0), inputInactive); + assert.equal(group.getEditorByIndex(1), input); editorGroupChangeListener.dispose(); part.dispose(); }); @@ -642,13 +642,13 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); - assert.equal(group.getEditor(0), input); - assert.equal(group.getEditor(1), inputInactive); + assert.equal(group.getEditorByIndex(0), input); + assert.equal(group.getEditorByIndex(1), inputInactive); group.moveEditor(inputInactive, rightGroup, { index: 0 }); assert.equal(group.count, 1); - assert.equal(group.getEditor(0), input); + assert.equal(group.getEditorByIndex(0), input); assert.equal(rightGroup.count, 1); - assert.equal(rightGroup.getEditor(0), inputInactive); + assert.equal(rightGroup.getEditorByIndex(0), inputInactive); part.dispose(); }); @@ -664,14 +664,14 @@ suite('EditorGroupsService', () => { await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); - assert.equal(group.getEditor(0), input); - assert.equal(group.getEditor(1), inputInactive); + assert.equal(group.getEditorByIndex(0), input); + assert.equal(group.getEditorByIndex(1), inputInactive); group.copyEditor(inputInactive, rightGroup, { index: 0 }); assert.equal(group.count, 2); - assert.equal(group.getEditor(0), input); - assert.equal(group.getEditor(1), inputInactive); + assert.equal(group.getEditorByIndex(0), input); + assert.equal(group.getEditorByIndex(1), inputInactive); assert.equal(rightGroup.count, 1); - assert.equal(rightGroup.getEditor(0), inputInactive); + assert.equal(rightGroup.getEditorByIndex(0), inputInactive); part.dispose(); }); @@ -685,11 +685,11 @@ suite('EditorGroupsService', () => { await group.openEditor(input); assert.equal(group.count, 1); - assert.equal(group.getEditor(0), input); + assert.equal(group.getEditorByIndex(0), input); await group.replaceEditors([{ editor: input, replacement: inputInactive }]); assert.equal(group.count, 1); - assert.equal(group.getEditor(0), inputInactive); + assert.equal(group.getEditorByIndex(0), inputInactive); part.dispose(); }); 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 ad798f5ce43..5d4221a7b02 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IEditorModel, EditorActivation } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; @@ -50,6 +50,10 @@ export class TestEditorControl extends BaseEditor { export class TestEditorInput extends EditorInput implements IFileEditorInput { public gotDisposed = false; + public gotSaved = false; + public gotSavedAs = false; + public gotReverted = false; + public dirty = false; private fails = false; constructor(private resource: URI) { super(); } @@ -66,6 +70,23 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput { setFailToOpen(): void { this.fails = true; } + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.gotSaved = true; + return Promise.resolve(true); + } + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.gotSavedAs = true; + return Promise.resolve(true); + } + revert(options?: IRevertOptions): Promise { + this.gotReverted = true; + this.gotSaved = false; + this.gotSavedAs = false; + return Promise.resolve(true); + } + isDirty(): boolean { + return this.dirty; + } dispose(): void { super.dispose(); this.gotDisposed = true; @@ -686,4 +707,45 @@ suite('EditorService', () => { let failingEditor = await service.openEditor(failingInput); assert.ok(!failingEditor); }); + + test('save, saveAll, revertAll', async function () { + const partInstantiator = workbenchInstantiationService(); + + const part = partInstantiator.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); + + const service: IEditorService = testInstantiationService.createInstance(EditorService); + + const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); + input1.dirty = true; + const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); + input2.dirty = true; + + const rootGroup = part.activeGroup; + + await part.whenRestored; + + await service.openEditor(input1, { pinned: true }); + await service.openEditor(input2, { pinned: true }); + + await service.save({ groupId: rootGroup.id, editor: input1 }); + assert.equal(input1.gotSaved, true); + + await service.save({ groupId: rootGroup.id, editor: input1 }, { saveAs: true }); + assert.equal(input1.gotSavedAs, true); + + await service.revertAll(); + assert.equal(input1.gotReverted, true); + + await service.saveAll(); + assert.equal(input1.gotSaved, true); + assert.equal(input2.gotSaved, true); + + await service.saveAll({ saveAs: true }); + assert.equal(input1.gotSavedAs, true); + assert.equal(input2.gotSavedAs, true); + }); }); diff --git a/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts index cb638f64361..f3b6c2bf21d 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProcessManager.ts @@ -406,7 +406,10 @@ export class MeasureExtHostLatencyAction extends Action { this._editorService.openEditor({ contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } } as IUntitledTextResourceInput); } - private static _print(m: ExtHostLatencyResult): string { + private static _print(m: ExtHostLatencyResult | null): string { + if (!m) { + return ''; + } return `${m.remoteAuthority ? `Authority: ${m.remoteAuthority}\n` : ``}Roundtrip latency: ${m.latency.toFixed(3)}ms\nUp: ${MeasureExtHostLatencyAction._printSpeed(m.up)}\nDown: ${MeasureExtHostLatencyAction._printSpeed(m.down)}\n`; } diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index a982e0161fc..6595d5b8ed0 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -14,7 +14,7 @@ import { isUndefinedOrNull } from 'vs/base/common/types'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { equals } from 'vs/base/common/objects'; -export const AutoSaveContext = new RawContextKey('config.files.autoSave', undefined); +export const AutoSaveAfterShortDelayContext = new RawContextKey('autoSaveAfterShortDelayContext', false); export interface IAutoSaveConfiguration { autoSaveDelay?: number; @@ -69,7 +69,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi private configuredAutoSaveOnFocusChange: boolean | undefined; private configuredAutoSaveOnWindowChange: boolean | undefined; - private autoSaveContext: IContextKey; + private autoSaveAfterShortDelayContext: IContextKey; private currentFilesAssociationConfig: { [key: string]: string; }; @@ -82,7 +82,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi ) { super(); - this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService); + this.autoSaveAfterShortDelayContext = AutoSaveAfterShortDelayContext.bindTo(contextKeyService); const configuration = configurationService.getValue(); @@ -108,7 +108,6 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi // Auto Save const autoSaveMode = configuration?.files?.autoSave || AutoSaveConfiguration.OFF; - this.autoSaveContext.set(autoSaveMode); switch (autoSaveMode) { case AutoSaveConfiguration.AFTER_DELAY: this.configuredAutoSaveDelay = configuration?.files?.autoSaveDelay; @@ -135,6 +134,8 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi break; } + this.autoSaveAfterShortDelayContext.set(this.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY); + // Emit as event this._onAutoSaveConfigurationChange.fire(this.getAutoSaveConfiguration()); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 429b7460fa6..5c3bb1e5f2e 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -482,10 +482,10 @@ export class HistoryService extends Disposable implements IHistoryService { } private handleEditorEventInHistory(editor?: IBaseEditor): void { - const input = editor?.input; - // Ensure we have not configured to exclude input - if (!input || !this.include(input)) { + // Ensure we have not configured to exclude input and don't track invalid inputs + const input = editor?.input; + if (!input || input.isDisposed() || !this.include(input)) { return; } @@ -592,10 +592,10 @@ export class HistoryService extends Disposable implements IHistoryService { // stack but we need to keep our currentTextEditorState up to date with // the navigtion that occurs. if (this.navigatingInStack) { - if (codeEditor && control?.input) { + if (codeEditor && control?.input && !control.input.isDisposed()) { this.currentTextEditorState = new TextEditorState(control.input, codeEditor.getSelection()); } else { - this.currentTextEditorState = null; // we navigated to a non text editor + this.currentTextEditorState = null; // we navigated to a non text or disposed editor } } @@ -603,15 +603,15 @@ export class HistoryService extends Disposable implements IHistoryService { else { // navigation inside text editor - if (codeEditor && control?.input) { + if (codeEditor && control?.input && !control.input.isDisposed()) { this.handleTextEditorEvent(control, codeEditor, event); } - // navigation to non-text editor + // navigation to non-text disposed editor else { this.currentTextEditorState = null; // at this time we have no active text editor view state - if (control?.input) { + if (control?.input && !control.input.isDisposed()) { this.handleNonTextEditorEvent(control); } } diff --git a/src/vs/workbench/services/keybinding/browser/keymapService.ts b/src/vs/workbench/services/keybinding/browser/keymapService.ts index 755e240d88e..4a22d72e0d3 100644 --- a/src/vs/workbench/services/keybinding/browser/keymapService.ts +++ b/src/vs/workbench/services/keybinding/browser/keymapService.ts @@ -335,6 +335,10 @@ export class BrowserKeyboardMapperFactoryBase { return true; } + if (standardKeyboardEvent.browserEvent.key === 'Dead' || standardKeyboardEvent.browserEvent.isComposing) { + return true; + } + const mapping = currentKeymap.mapping[standardKeyboardEvent.code]; if (!mapping) { diff --git a/src/vs/workbench/services/mode/common/workbenchModeService.ts b/src/vs/workbench/services/mode/common/workbenchModeService.ts index f097879bc0d..c6033d4ef98 100644 --- a/src/vs/workbench/services/mode/common/workbenchModeService.ts +++ b/src/vs/workbench/services/mode/common/workbenchModeService.ts @@ -105,7 +105,7 @@ export class WorkbenchModeServiceImpl extends ModeServiceImpl { this._configurationService = configurationService; this._extensionService = extensionService; - languagesExtPoint.setHandler((extensions: IExtensionPointUser[]) => { + languagesExtPoint.setHandler((extensions: readonly IExtensionPointUser[]) => { let allValidLanguages: ILanguageExtensionPoint[] = []; for (let i = 0, len = extensions.length; i < len; i++) { diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index fe4ddff014d..400006f193f 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -8,8 +8,8 @@ import { URI } from 'vs/base/common/uri'; import { Emitter, AsyncEmitter } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent } from 'vs/workbench/services/textfile/common/textfiles'; -import { IRevertOptions } from 'vs/workbench/common/editor'; +import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason, IRevertOptions } from 'vs/workbench/common/editor'; import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileService, FileOperationError, FileOperationResult, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files'; @@ -477,7 +477,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex //#region save/revert - async save(resource: URI, options?: ISaveOptions): Promise { + async save(resource: URI, options?: ITextFileSaveOptions): Promise { // Run a forced save if we detect the file is not dirty so that save participants can still run if (options?.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) { @@ -496,9 +496,9 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return result.results.length === 1 && !!result.results[0].success; } - saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise; - saveAll(resources: URI[], options?: ISaveOptions): Promise; - saveAll(arg1?: boolean | URI[], options?: ISaveOptions): Promise { + saveAll(includeUntitled?: boolean, options?: ITextFileSaveOptions): Promise; + saveAll(resources: URI[], options?: ITextFileSaveOptions): Promise; + saveAll(arg1?: boolean | URI[], options?: ITextFileSaveOptions): Promise { // get all dirty let toSave: URI[] = []; @@ -522,7 +522,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return this.doSaveAll(filesToSave, untitledToSave, options); } - private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): Promise { + private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ITextFileSaveOptions): Promise { // Handle files first that can just be saved const result = await this.doSaveAllFiles(fileResources, options); @@ -627,7 +627,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return options; } - private async doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): Promise { + private async doSaveAllFiles(resources?: URI[], options: ITextFileSaveOptions = Object.create(null)): Promise { const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : undefined /* Save All */) .filter(model => { if ((model.hasState(ModelState.CONFLICT) || model.hasState(ModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE)) { @@ -675,7 +675,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return this.getFileModels(resources).filter(model => model.isDirty()); } - async saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): Promise { + async saveAs(resource: URI, targetResource?: URI, options?: ITextFileSaveOptions): Promise { // Get to target resource if (!targetResource) { @@ -702,7 +702,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return this.doSaveAs(resource, targetResource, options); } - private async doSaveAs(resource: URI, target: URI, options?: ISaveOptions): Promise { + private async doSaveAs(resource: URI, target: URI, options?: ITextFileSaveOptions): Promise { // Retrieve text model from provided resource if any let model: ITextFileEditorModel | UntitledTextEditorModel | undefined; @@ -736,7 +736,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return target; } - private async doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledTextEditorModel, resource: URI, target: URI, options?: ISaveOptions): Promise { + private async doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledTextEditorModel, resource: URI, target: URI, options?: ITextFileSaveOptions): Promise { // Prefer an existing model if it is already loaded for the given target resource let targetExists: boolean = false; @@ -866,7 +866,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex await Promise.all(fileModels.map(async model => { try { - await model.revert(options?.soft); + await model.revert(options); if (!model.isDirty()) { const result = mapResourceToResult.get(model.resource); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 7dba20135d7..f125c5235e5 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -11,8 +11,8 @@ import { URI } from 'vs/base/common/uri'; import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITextFileService, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; -import { EncodingMode } from 'vs/workbench/common/editor'; +import { ITextFileService, ModelState, ITextFileEditorModel, ISaveErrorHandler, ISaveParticipant, StateChange, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { EncodingMode, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED } from 'vs/platform/files/common/files'; @@ -252,9 +252,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.backupFileService.hasBackupSync(this.resource, this.versionId); } - async revert(soft?: boolean): Promise { + async revert(options?: IRevertOptions): Promise { if (!this.isResolved()) { - return; + return false; } // Cancel any running auto-save @@ -265,7 +265,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil const undo = this.setDirty(false); // Force read from disk unless reverting soft - if (!soft) { + const softUndo = options?.soft; + if (!softUndo) { try { await this.load({ forceReadFromDisk: true }); } catch (error) { @@ -284,6 +285,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (wasDirty) { this._onDidChangeDirty.fire(); } + + return true; } async load(options?: ILoadOptions): Promise { @@ -612,9 +615,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.autoSaveDisposable.value = toDisposable(() => clearTimeout(handle)); } - async save(options: ISaveOptions = Object.create(null)): Promise { + async save(options: ITextFileSaveOptions = Object.create(null)): Promise { if (!this.isResolved()) { - return; + return false; } this.logService.trace('save() - enter', this.resource); @@ -622,10 +625,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Cancel any currently running auto saves to make this the one that succeeds this.autoSaveDisposable.clear(); - return this.doSave(this.versionId, options); + await this.doSave(this.versionId, options); + + return true; } - private doSave(versionId: number, options: ISaveOptions): Promise { + private doSave(versionId: number, options: ITextFileSaveOptions): Promise { if (isUndefinedOrNull(options.reason)) { options.reason = SaveReason.EXPLICIT; } diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index d07dc93f41e..51338221b40 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { Event, IWaitUntil } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IEncodingSupport, IRevertOptions, IModeSupport } from 'vs/workbench/common/editor'; +import { IEncodingSupport, IModeSupport, ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult, FileOperation } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -323,13 +323,6 @@ export interface IResult { success?: boolean; } -export const enum SaveReason { - EXPLICIT = 1, - AUTO = 2, - FOCUS_CHANGE = 3, - WINDOW_CHANGE = 4 -} - export const enum LoadReason { EDITOR = 1, REFERENCE = 2, @@ -421,14 +414,10 @@ export interface ITextFileEditorModelManager { disposeModel(model: ITextFileEditorModel): void; } -export interface ISaveOptions { - force?: boolean; - reason?: SaveReason; +export interface ITextFileSaveOptions extends ISaveOptions { overwriteReadonly?: boolean; overwriteEncoding?: boolean; - skipSaveParticipants?: boolean; writeElevated?: boolean; - availableFileSystems?: readonly string[]; } export interface ILoadOptions { @@ -458,11 +447,11 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport updatePreferredEncoding(encoding: string | undefined): void; - save(options?: ISaveOptions): Promise; + save(options?: ITextFileSaveOptions): Promise; load(options?: ILoadOptions): Promise; - revert(soft?: boolean): Promise; + revert(options?: IRevertOptions): Promise; backup(target?: URI): Promise; diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts index 8ccf5a25c5d..3d8c90ba250 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts @@ -263,7 +263,7 @@ suite('Files - TextFileEditorModel', () => { assert.equal(accessor.workingCopyService.dirtyCount, 1); assert.equal(accessor.workingCopyService.isDirty(model.resource), true); - await model.revert(true /* soft revert */); + await model.revert({ soft: true }); assert.ok(!model.isDirty()); assert.equal(model.textEditorModel!.getValue(), 'foo'); assert.equal(eventCounter, 1); @@ -300,7 +300,7 @@ suite('Files - TextFileEditorModel', () => { model.textEditorModel!.setValue('foo'); assert.ok(model.isDirty()); - await model.revert(true /* soft revert */); + await model.revert({ soft: true }); assert.ok(!model.isDirty()); model.onDidStateChange(e => { diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts index 0494cbf139b..f4db513644f 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts @@ -286,7 +286,7 @@ suite('Files - TextFileEditorModelManager', () => { model.textEditorModel!.setValue('make dirty'); manager.disposeModel((model as TextFileEditorModel)); assert.ok(!model.isDisposed()); - model.revert(true); + model.revert({ soft: true }); manager.disposeModel((model as TextFileEditorModel)); assert.ok(model.isDisposed()); manager.dispose(); diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts index fcb13e5c16c..32af9851cea 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts @@ -160,7 +160,6 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe untitledInputs.forEach(input => { if (input) { input.revert(); - input.dispose(); reverted.push(input.getResource()); } diff --git a/src/vs/workbench/services/url/electron-browser/urlService.ts b/src/vs/workbench/services/url/electron-browser/urlService.ts index 73fa15e1f8e..c1485a6ec59 100644 --- a/src/vs/workbench/services/url/electron-browser/urlService.ts +++ b/src/vs/workbench/services/url/electron-browser/urlService.ts @@ -8,7 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { URLHandlerChannel } from 'vs/platform/url/common/urlIpc'; import { URLService } from 'vs/platform/url/node/urlService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IOpenerService, IOpener, matchesScheme } from 'vs/platform/opener/common/opener'; import product from 'vs/platform/product/common/product'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService'; @@ -20,7 +20,7 @@ export interface IRelayOpenURLOptions extends IOpenURLOptions { openExternal?: boolean; } -export class RelayURLService extends URLService implements IURLHandler { +export class RelayURLService extends URLService implements IURLHandler, IOpener { private urlService: IURLService; @@ -51,11 +51,15 @@ export class RelayURLService extends URLService implements IURLHandler { return uri.with({ query }); } - async open(resource: URI, options?: IRelayOpenURLOptions): Promise { - if (resource.scheme !== product.urlProtocol) { + async open(resource: URI | string, options?: IRelayOpenURLOptions): Promise { + + if (!matchesScheme(resource, product.urlProtocol)) { return false; } + if (typeof resource === 'string') { + resource = URI.parse(resource); + } return await this.urlService.open(resource, options); } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index fe2f33adfa3..fd4da1c641f 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -29,6 +29,7 @@ export interface IWorkingCopy { //#endregion + readonly resource: URI; readonly capabilities: WorkingCopyCapabilities; diff --git a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts index 63e66f3a6b5..eae32007ddc 100644 --- a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { IWorkspacesService, isUntitledWorkspace, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesService, isUntitledWorkspace, IWorkspaceIdentifier, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -124,7 +124,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi // Save: save workspace, but do not veto unload if path provided case ConfirmResult.SAVE: { const newWorkspacePath = await this.pickNewWorkspacePath(); - if (!newWorkspacePath) { + if (!newWorkspacePath || !hasWorkspaceFileExtension(newWorkspacePath)) { return true; // keep veto if no target was provided } diff --git a/src/vs/workbench/test/common/editor/editorGroups.test.ts b/src/vs/workbench/test/common/editor/editorGroups.test.ts index 46805b2c9da..6cacfb90746 100644 --- a/src/vs/workbench/test/common/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/common/editor/editorGroups.test.ts @@ -187,19 +187,79 @@ suite('Workbench editor groups', () => { assert.equal(clone.isActive(input3), true); }); - test('contains() with diff editor support', function () { + test('contains()', function () { const group = createGroup(); const input1 = input(); const input2 = input(); - const diffInput = new DiffEditorInput('name', 'description', input1, input2); + const diffInput1 = new DiffEditorInput('name', 'description', input1, input2); + const diffInput2 = new DiffEditorInput('name', 'description', input2, input1); + + group.openEditor(input1, { pinned: true, active: true }); + + assert.equal(group.contains(input1), true); + assert.equal(group.contains(input1, true), true); + assert.equal(group.contains(input2), false); + assert.equal(group.contains(input2, true), false); + assert.equal(group.contains(diffInput1), false); + assert.equal(group.contains(diffInput2), false); group.openEditor(input2, { pinned: true, active: true }); + assert.equal(group.contains(input1), true); assert.equal(group.contains(input2), true); - assert.equal(group.contains(diffInput), false); - assert.equal(group.contains(diffInput, true), true); + assert.equal(group.contains(diffInput1), false); + assert.equal(group.contains(diffInput2), false); + + group.openEditor(diffInput1, { pinned: true, active: true }); + + assert.equal(group.contains(input1), true); + assert.equal(group.contains(input2), true); + assert.equal(group.contains(diffInput1), true); + assert.equal(group.contains(diffInput2), false); + + group.openEditor(diffInput2, { pinned: true, active: true }); + + assert.equal(group.contains(input1), true); + assert.equal(group.contains(input2), true); + assert.equal(group.contains(diffInput1), true); + assert.equal(group.contains(diffInput2), true); + + group.closeEditor(input1); + + assert.equal(group.contains(input1), false); + assert.equal(group.contains(input1, true), true); + assert.equal(group.contains(input2), true); + assert.equal(group.contains(diffInput1), true); + assert.equal(group.contains(diffInput2), true); + + group.closeEditor(input2); + + assert.equal(group.contains(input1), false); + assert.equal(group.contains(input1, true), true); + assert.equal(group.contains(input2), false); + assert.equal(group.contains(input2, true), true); + assert.equal(group.contains(diffInput1), true); + assert.equal(group.contains(diffInput2), true); + + group.closeEditor(diffInput1); + + assert.equal(group.contains(input1), false); + assert.equal(group.contains(input1, true), true); + assert.equal(group.contains(input2), false); + assert.equal(group.contains(input2, true), true); + assert.equal(group.contains(diffInput1), false); + assert.equal(group.contains(diffInput2), true); + + group.closeEditor(diffInput2); + + assert.equal(group.contains(input1), false); + assert.equal(group.contains(input1, true), false); + assert.equal(group.contains(input2), false); + assert.equal(group.contains(input2, true), false); + assert.equal(group.contains(diffInput1), false); + assert.equal(group.contains(diffInput2), false); }); test('group serialization', function () { @@ -1162,47 +1222,6 @@ suite('Workbench editor groups', () => { assert.equal(group1.getEditors()[1].matches(serializableInput2), true); }); - test('Multiple Editors - Resources', function () { - const group1 = createGroup(); - const group2 = createGroup(); - - const input1Resource = URI.file('/hello/world.txt'); - const input1ResourceUpper = URI.file('/hello/WORLD.txt'); - const input1 = input(undefined, false, input1Resource); - group1.openEditor(input1); - - assert.ok(group1.contains(input1Resource)); - assert.equal(group1.getEditor(input1Resource), input1); - - assert.ok(!group1.getEditor(input1ResourceUpper)); - assert.ok(!group1.contains(input1ResourceUpper)); - - group2.openEditor(input1); - group1.closeEditor(input1); - - assert.ok(!group1.contains(input1Resource)); - assert.ok(!group1.getEditor(input1Resource)); - assert.ok(!group1.getEditor(input1ResourceUpper)); - assert.ok(group2.contains(input1Resource)); - assert.equal(group2.getEditor(input1Resource), input1); - - const input1ResourceClone = URI.file('/hello/world.txt'); - const input1Clone = input(undefined, false, input1ResourceClone); - group1.openEditor(input1Clone); - - assert.ok(group1.contains(input1Resource)); - - group2.closeEditor(input1); - - assert.ok(group1.contains(input1Resource)); - assert.equal(group1.getEditor(input1Resource), input1Clone); - assert.ok(!group2.contains(input1Resource)); - - group1.closeEditor(input1Clone); - - assert.ok(!group1.contains(input1Resource)); - }); - test('Multiple Editors - Editor Dispose', function () { const group1 = createGroup(); const group2 = createGroup(); diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts index d85eae55ebf..313272d338f 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts @@ -10,7 +10,7 @@ import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbe import { MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostDocumentSaveParticipant'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; -import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; import * as vscode from 'vscode'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts index 502d3fae8e9..40b3489c80f 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts @@ -13,7 +13,8 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { ITextFileService, SaveReason, IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { SaveReason } from 'vs/workbench/common/editor'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; class ServiceAccessor { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 1e87e03eeaa..5a73dc9c71d 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -11,7 +11,7 @@ import * as resources from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; @@ -57,7 +57,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor, ISaveEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; @@ -809,7 +809,7 @@ export class TestEditorGroup implements IEditorGroupView { return []; } - getEditor(_index: number): IEditorInput { + getEditorByIndex(_index: number): IEditorInput { throw new Error('not implemented'); } @@ -921,6 +921,18 @@ export class TestEditorService implements EditorServiceImpl { createInput(_input: IResourceInput | IUntitledTextResourceInput | IResourceDiffInput | IResourceSideBySideInput): IEditorInput { throw new Error('not implemented'); } + + save(editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { + throw new Error('Method not implemented.'); + } + + saveAll(options?: ISaveEditorsOptions): Promise { + throw new Error('Method not implemented.'); + } + + revertAll(options?: IRevertOptions): Promise { + throw new Error('Method not implemented.'); + } } export class TestFileService implements IFileService { @@ -1375,7 +1387,7 @@ export class TestElectronService implements IElectronService { async setRepresentedFilename(path: string): Promise { } async setDocumentEdited(edited: boolean): Promise { } async openExternal(url: string): Promise { return false; } - async updateTouchBar(items: { id: string; title: string | { value: string; original: string; }; category?: string | { value: string; original: string; } | undefined; iconLocation?: { dark: UriComponents; light?: { readonly scheme: string; readonly authority: string; readonly path: string; readonly query: string; readonly fragment: string; readonly fsPath: string; with: {}; toString: {}; toJSON: {}; } | undefined; } | undefined; precondition?: { getType: {}; equals: {}; evaluate: {}; serialize: {}; keys: {}; map: {}; negate: {}; } | undefined; toggled?: { getType: {}; equals: {}; evaluate: {}; serialize: {}; keys: {}; map: {}; negate: {}; } | undefined; }[][]): Promise { } + async updateTouchBar(items: { id: string; title: string | { value: string; original: string; }; category?: string | { value: string; original: string; } | undefined; iconLocation?: { dark?: UriComponents; light?: { readonly scheme: string; readonly authority: string; readonly path: string; readonly query: string; readonly fragment: string; readonly fsPath: string; with: {}; toString: {}; toJSON: {}; } | undefined; } | undefined; precondition?: { getType: {}; equals: {}; evaluate: {}; serialize: {}; keys: {}; map: {}; negate: {}; } | undefined; toggled?: { getType: {}; equals: {}; evaluate: {}; serialize: {}; keys: {}; map: {}; negate: {}; } | undefined; }[][]): Promise { } async newWindowTab(): Promise { } async showPreviousWindowTab(): Promise { } async showNextWindowTab(): Promise { } diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index 2dc548e5cb3..4739d8a9917 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -19,7 +19,7 @@ const STEP_IN = `.debug-toolbar .action-label[title*="Step Into"]`; const STEP_OUT = `.debug-toolbar .action-label[title*="Step Out"]`; const CONTINUE = `.debug-toolbar .action-label[title*="Continue"]`; const GLYPH_AREA = '.margin-view-overlays>:nth-child'; -const BREAKPOINT_GLYPH = '.debug-breakpoint'; +const BREAKPOINT_GLYPH = '.codicon-debug-breakpoint'; const PAUSE = `.debug-toolbar .action-label[title*="Pause"]`; const DEBUG_STATUS_BAR = `.statusbar.debugging`; const NOT_DEBUG_STATUS_BAR = `.statusbar:not(debugging)`; diff --git a/yarn.lock b/yarn.lock index e9078a4f13f..d9bdce0cd9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9369,6 +9369,11 @@ xterm-addon-web-links@0.2.1: resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.2.1.tgz#6d1f2ce613e09870badf17615e7a1170a31542b2" integrity sha512-2KnHtiq0IG7hfwv3jw2/jQeH1RBk2d5CH4zvgwQe00rLofSJqSfgnJ7gwowxxpGHrpbPr6Lv4AmH/joaNw2+HQ== +xterm-addon-webgl@0.4.0-beta6: + version "0.4.0-beta6" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta6.tgz#23d152a0467d8b1f96ab3da7ac9a49bfa0b08c98" + integrity sha512-gtM8XtRyrNFCtxHBIU3pqTlBeAi5VOoymJAXKQQ7RsHEVJX79OTk1dQ9Q6Ow14+REGwQU/zFECV050jbLTfvrQ== + xterm@4.3.0-beta17: version "4.3.0-beta17" resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta17.tgz#c038cc00cb5be33d2a5f083255c329d9ed186565"