diff --git a/build/package.json b/build/package.json index 96cb5414382..d9523ae2b92 100644 --- a/build/package.json +++ b/build/package.json @@ -43,7 +43,7 @@ "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^3.9.0-dev.20200413", + "typescript": "^3.9.0-dev.20200420", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.5.4", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index 3bb4b7744c7..21246ba1b1d 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2458,10 +2458,10 @@ typescript@^3.0.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^3.9.0-dev.20200413: - version "3.9.0-dev.20200413" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200413.tgz#dd38331435f5c30867f19c94fe7cfc567ec13e20" - integrity sha512-wpPljcRc3uLTVpQ8mtKTkS42ipVnW1E7lU5iII+JSKaUDH1ymeo3duvMz8o2sCRUDoZR0SE0SLcinaZ1q4gD7A== +typescript@^3.9.0-dev.20200420: + version "3.9.0-dev.20200420" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200420.tgz#99c2bc0936dbf4479b0b5260d80475ed494b1532" + integrity sha512-36MW6V+oXNnsSgliSjUWvtOkO21g9+iFGHPFv+O06HsCl3dcuqzBac17m8xuOuWo1LUlEgS6yAnD9fiVgvmCfg== typical@^4.0.0: version "4.0.0" diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index bc826cf012c..53217156be4 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -269,20 +269,26 @@ class RemoteSourceProviderQuickPick { @throttle async query(): Promise { this.quickpick.busy = true; - const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || []; - this.quickpick.busy = false; - if (remoteSources.length === 0) { - this.quickpick.items = [{ - label: localize('none found', "No remote repositories found."), - alwaysShow: true - }]; - } else { - this.quickpick.items = remoteSources.map(remoteSource => ({ - label: remoteSource.name, - description: remoteSource.url, - remote: remoteSource - })); + try { + const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || []; + + if (remoteSources.length === 0) { + this.quickpick.items = [{ + label: localize('none found', "No remote repositories found."), + alwaysShow: true + }]; + } else { + this.quickpick.items = remoteSources.map(remoteSource => ({ + label: remoteSource.name, + description: remoteSource.url, + remote: remoteSource + })); + } + } catch (err) { + this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }]; + } finally { + this.quickpick.busy = false; } } diff --git a/package.json b/package.json index 4de717d9273..ae5d4d88ee9 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "^3.9.0-dev.20200413", + "typescript": "^3.9.0-dev.20200420", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", diff --git a/src/vs/base/browser/ui/aria/aria.ts b/src/vs/base/browser/ui/aria/aria.ts index 2867c1256dc..b42fbf94ac2 100644 --- a/src/vs/base/browser/ui/aria/aria.ts +++ b/src/vs/base/browser/ui/aria/aria.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./aria'; -import * as nls from 'vs/nls'; import { isMacintosh } from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; @@ -34,47 +33,26 @@ export function setARIAContainer(parent: HTMLElement) { /** * Given the provided message, will make sure that it is read as alert to screen readers. */ -export function alert(msg: string, disableRepeat?: boolean): void { - insertMessage(alertContainer, msg, disableRepeat); +export function alert(msg: string): void { + insertMessage(alertContainer, msg); } /** * Given the provided message, will make sure that it is read as status to screen readers. */ -export function status(msg: string, disableRepeat?: boolean): void { +export function status(msg: string): void { if (isMacintosh) { - alert(msg, disableRepeat); // VoiceOver does not seem to support status role + alert(msg); // VoiceOver does not seem to support status role } else { - insertMessage(statusContainer, msg, disableRepeat); + insertMessage(statusContainer, msg); } } -let repeatedTimes = 0; -let prevText: string | undefined = undefined; -function insertMessage(target: HTMLElement, msg: string, disableRepeat?: boolean): void { +function insertMessage(target: HTMLElement, msg: string): void { if (!ariaContainer) { return; } - // If the same message should be inserted that is already present, a screen reader would - // not announce this message because it matches the previous one. As a workaround, we - // alter the message with the number of occurences unless this is explicitly disabled - // via the disableRepeat flag. - if (!disableRepeat) { - if (prevText === msg) { - repeatedTimes++; - } else { - prevText = msg; - repeatedTimes = 0; - } - - switch (repeatedTimes) { - case 0: break; - case 1: msg = nls.localize('repeated', "{0} (occurred again)", msg); break; - default: msg = nls.localize('repeatedNtimes', "{0} (occurred {1} times)", msg, repeatedTimes); break; - } - } - dom.clearNode(target); target.textContent = msg; diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 3b34681a2ce..67fdc1f90a2 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -673,7 +673,7 @@ class QuickPick extends QuickInput implements IQuickPi if (this.canSelectMany) { this.ui.list.domFocus(); } - event.preventDefault(); + dom.EventHelper.stop(event, true); break; case KeyCode.UpArrow: if (this.ui.list.getFocusedElements().length) { @@ -684,21 +684,21 @@ class QuickPick extends QuickInput implements IQuickPi if (this.canSelectMany) { this.ui.list.domFocus(); } - event.preventDefault(); + dom.EventHelper.stop(event, true); break; case KeyCode.PageDown: this.ui.list.focus(QuickInputListFocus.NextPage); if (this.canSelectMany) { this.ui.list.domFocus(); } - event.preventDefault(); + dom.EventHelper.stop(event, true); break; case KeyCode.PageUp: this.ui.list.focus(QuickInputListFocus.PreviousPage); if (this.canSelectMany) { this.ui.list.domFocus(); } - event.preventDefault(); + dom.EventHelper.stop(event, true); break; case KeyCode.RightArrow: if (!this._canAcceptInBackground) { @@ -719,13 +719,13 @@ class QuickPick extends QuickInput implements IQuickPi case KeyCode.Home: if (event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) { this.ui.list.focus(QuickInputListFocus.First); - event.preventDefault(); + dom.EventHelper.stop(event, true); } break; case KeyCode.End: if (event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) { this.ui.list.focus(QuickInputListFocus.Last); - event.preventDefault(); + dom.EventHelper.stop(event, true); } break; } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 74cd460f5e3..d49d329a098 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, ipcMain as ipc, systemPreferences, shell, Event, contentTracing, protocol, powerMonitor, IpcMainEvent, BrowserWindow, dialog } from 'electron'; +import { app, ipcMain as ipc, systemPreferences, shell, Event, contentTracing, protocol, powerMonitor, IpcMainEvent, BrowserWindow, dialog, session } from 'electron'; import { IProcessEnvironment, isWindows, isMacintosh } from 'vs/base/common/platform'; import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; @@ -129,7 +129,7 @@ export class CodeApplication extends Disposable { } }); - // Security related measures (https://electronjs.org/docs/tutorial/security) + //#region Security related measures (https://electronjs.org/docs/tutorial/security) // // !!! DO NOT CHANGE without consulting the documentation !!! // @@ -212,8 +212,18 @@ export class CodeApplication extends Disposable { shell.openExternal(url); }); + + session.defaultSession.setPermissionRequestHandler((webContents, permission /* 'media' | 'geolocation' | 'notifications' | 'midiSysex' | 'pointerLock' | 'fullscreen' | 'openExternal' */, callback) => { + return callback(false); + }); + + session.defaultSession.setPermissionCheckHandler((webContents, permission /* 'media' */) => { + return false; + }); }); + //#endregion + let macOpenFileURIs: IWindowOpenable[] = []; let runningTimeout: NodeJS.Timeout | null = null; app.on('open-file', (event: Event, path: string) => { diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 9fd10acf1e8..4821653f75b 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -409,7 +409,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { return callback({ cancel: true }); } - return callback({ cancel: false, responseHeaders }); + return callback({ cancel: false }); }); // Remember that we loaded diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index e62aa46a502..7a219e7494d 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -422,7 +422,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._matchesCount.appendChild(document.createTextNode(label)); - alertFn(this._getAriaLabel(label, this._state.currentMatch, this._state.searchString), true); + alertFn(this._getAriaLabel(label, this._state.currentMatch, this._state.searchString)); MAX_MATCHES_COUNT_WIDTH = Math.max(MAX_MATCHES_COUNT_WIDTH, this._matchesCount.clientWidth); } diff --git a/src/vs/editor/contrib/hover/hover.css b/src/vs/editor/contrib/hover/hover.css index 8f35b9aec75..cb5bc10f4a2 100644 --- a/src/vs/editor/contrib/hover/hover.css +++ b/src/vs/editor/contrib/hover/hover.css @@ -30,7 +30,7 @@ } .monaco-editor-hover .markdown-hover > .hover-contents:not(.code-hover-contents) hr { - min-width: 100vw; + min-width: 100%; } .monaco-editor-hover p, diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 81ec255e65b..5ddf08de7bf 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -48,7 +48,13 @@ export async function asJson(context: IRequestContext): Promise; + version: number; + getText(): string; + getText(range: Range): string; + offsetAt(position: Position): number; + positionAt(offset: number): Position; + locationAt(positionOrRange: Position | Range): Location; + positionAt(location: Location): Position; } export interface NotebookEditorCellEdit { diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index b6bf77177b8..1963845b781 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -451,7 +451,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: COMMENTS_VIEW_ID, name: COMMENTS_VIEW_TITLE, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + storageId: COMMENTS_VIEW_TITLE, hideIfEmpty: true, order: 10, }, ViewContainerLocation.Panel); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 0584726e363..cb72f4d409e 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -405,8 +405,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha private static _inflateSuggestDto(defaultRange: IRange | { insert: IRange, replace: IRange }, data: ISuggestDataDto): modes.CompletionItem { return { - label: data[ISuggestDataDtoField.label2] || data[ISuggestDataDtoField.label], - kind: data[ISuggestDataDtoField.kind] || modes.CompletionItemKind.Property, + label: data[ISuggestDataDtoField.label2] ?? data[ISuggestDataDtoField.label], + kind: data[ISuggestDataDtoField.kind] ?? modes.CompletionItemKind.Property, tags: data[ISuggestDataDtoField.kindModifier], detail: data[ISuggestDataDtoField.detail], documentation: data[ISuggestDataDtoField.documentation], @@ -414,7 +414,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha filterText: data[ISuggestDataDtoField.filterText], preselect: data[ISuggestDataDtoField.preselect], insertText: typeof data.h === 'undefined' ? data[ISuggestDataDtoField.label] : data.h, - range: data[ISuggestDataDtoField.range] || defaultRange, + range: data[ISuggestDataDtoField.range] ?? defaultRange, insertTextRules: data[ISuggestDataDtoField.insertTextRules], commitCharacters: data[ISuggestDataDtoField.commitCharacters], additionalTextEdits: data[ISuggestDataDtoField.additionalTextEdits], diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 354fbf83287..7a0e83d8ba8 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -320,7 +320,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { name: title, extensionId, ctorDescriptor: new SyncDescriptor( ViewPaneContainer, - [id, `${id}.state`, { mergeViewWithContainerWhenSingleView: true }] + [id, { mergeViewWithContainerWhenSingleView: true }] ), hideIfEmpty: true, order, diff --git a/src/vs/workbench/api/common/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts index a295f646dd3..756f1a03f84 100644 --- a/src/vs/workbench/api/common/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -181,6 +181,10 @@ export class ExtHostDocumentData extends MirrorTextModel { throw new Error('Invalid argument'); } + if (this._lines.length === 0) { + return position.with(0, 0); + } + let { line, character } = position; let hasChanged = false; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 96785eb5f41..26084cfab8c 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -954,7 +954,7 @@ class SuggestAdapter { // [extHostProtocol.ISuggestDataDtoField.label]: item.label, [extHostProtocol.ISuggestDataDtoField.label2]: item.label2, - [extHostProtocol.ISuggestDataDtoField.kind]: item.kind ? typeConvert.CompletionItemKind.from(item.kind) : undefined, + [extHostProtocol.ISuggestDataDtoField.kind]: item.kind !== undefined ? typeConvert.CompletionItemKind.from(item.kind) : undefined, [extHostProtocol.ISuggestDataDtoField.kindModifier]: item.tags && item.tags.map(typeConvert.CompletionItemTag.from), [extHostProtocol.ISuggestDataDtoField.detail]: item.detail, [extHostProtocol.ISuggestDataDtoField.documentation]: typeof item.documentation === 'undefined' ? undefined : typeConvert.MarkdownString.fromStrict(item.documentation), diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 5e2ce8a9c3a..8db82c8a67b 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -10,12 +10,14 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Disposable as VSCodeDisposable } from './extHostTypes'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; +import { NotImplementedProxy } from 'vs/base/common/types'; interface IObservable { proxy: T; @@ -40,39 +42,48 @@ function getObservable(obj: T): IObservable { export class ExtHostCell extends Disposable implements vscode.NotebookCell { - private originalSource: string[]; + // private originalSource: string[]; private _outputs: any[]; private _onDidChangeOutputs = new Emitter[]>(); onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; - private _textDocument: vscode.TextDocument | undefined; - private _initalVersion: number = -1; + // private _textDocument: vscode.TextDocument | undefined; + // private _initalVersion: number = -1; private _outputMapping = new Set(); private _metadata: vscode.NotebookCellMetadata; private _metadataChangeListener: IDisposable; + private _documentData: ExtHostDocumentData; + + get document(): vscode.TextDocument { + return this._documentData.document; + } + get source() { - if (this._textDocument && this._initalVersion !== this._textDocument?.version) { - return this._textDocument.getText(); - } else { - return this.originalSource.join('\n'); - } + // todo@jrieken remove this + return this._documentData.getText(); } constructor( - private viewType: string, - private documentUri: URI, + private readonly viewType: string, + private readonly documentUri: URI, readonly handle: number, readonly uri: URI, - private _content: string, + content: string, public readonly cellKind: CellKind, public language: string, outputs: any[], _metadata: vscode.NotebookCellMetadata | undefined, - private _proxy: MainThreadNotebookShape + private _proxy: MainThreadNotebookShape, ) { super(); - this.originalSource = this._content.split(/\r|\n|\r\n/g); + this._documentData = new ExtHostDocumentData( + new class extends NotImplementedProxy('document') { }, + uri, + content.split(/\r|\n|\r\n/g), '\n', + language, 0, false + ); + this._outputs = outputs; const observableMetadata = getObservable(_metadata || {}); @@ -125,26 +136,19 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { return this._proxy.$updateNotebookCellMetadata(this.viewType, this.documentUri, this.handle, this._metadata); } - getContent(): string { - if (this._textDocument && this._initalVersion !== this._textDocument?.version) { - return this._textDocument.getText(); - } else { - return this.originalSource.join('\n'); - } - } - - attachTextDocument(document: vscode.TextDocument) { - this._textDocument = document; - this._initalVersion = this._textDocument.version; + attachTextDocument(document: ExtHostDocumentData) { + this._documentData = document; + // this._initalVersion = this._documentData.version; } detachTextDocument() { - if (this._textDocument && this._textDocument.version !== this._initalVersion) { - this.originalSource = this._textDocument.getText().split(/\r|\n|\r\n/g); - } + // no-op? keep stale document until new comes along? - this._textDocument = undefined; - this._initalVersion = -1; + // if (this._textDocument && this._textDocument.version !== this._initalVersion) { + // this.originalSource = this._textDocument.getText().split(/\r|\n|\r\n/g); + // } + // this._textDocument = undefined; + // this._initalVersion = -1; } } @@ -256,10 +260,10 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo let cellDtos = splice[2]; let newCells = cellDtos.map(cell => { const extCell = new ExtHostCell(this.viewType, this.uri, cell.handle, URI.revive(cell.uri), cell.source.join('\n'), cell.cellKind, cell.language, cell.outputs, cell.metadata, this._proxy); - const document = this._documentsAndEditors.getDocument(URI.revive(cell.uri)); + const documentData = this._documentsAndEditors.getDocument(URI.revive(cell.uri)); - if (document) { - extCell.attachTextDocument(document.document); + if (documentData) { + extCell.attachTextDocument(documentData); } if (!this._cellDisposableMapping.has(extCell.handle)) { @@ -366,15 +370,15 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo return this.cells.find(cell => cell.handle === cellHandle); } - attachCellTextDocument(textDocument: vscode.TextDocument) { - let cell = this.cells.find(cell => cell.uri.toString() === textDocument.uri.toString()); + attachCellTextDocument(textDocument: ExtHostDocumentData) { + let cell = this.cells.find(cell => cell.uri.toString() === textDocument.document.uri.toString()); if (cell) { cell.attachTextDocument(textDocument); } } - detachCellTextDocument(textDocument: vscode.TextDocument) { - let cell = this.cells.find(cell => cell.uri.toString() === textDocument.uri.toString()); + detachCellTextDocument(textDocument: ExtHostDocumentData) { + let cell = this.cells.find(cell => cell.uri.toString() === textDocument.document.uri.toString()); if (cell) { cell.detachTextDocument(); } @@ -468,22 +472,22 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook ) { super(); this._register(this._documentsAndEditors.onDidAddDocuments(documents => { - for (const { document: textDocument } of documents) { - let data = CellUri.parse(textDocument.uri); + for (const documentData of documents) { + let data = CellUri.parse(documentData.document.uri); if (data) { if (this.document.uri.toString() === data.notebook.toString()) { - document.attachCellTextDocument(textDocument); + document.attachCellTextDocument(documentData); } } } })); this._register(this._documentsAndEditors.onDidRemoveDocuments(documents => { - for (const { document: textDocument } of documents) { - let data = CellUri.parse(textDocument.uri); + for (const documentData of documents) { + let data = CellUri.parse(documentData.document.uri); if (data) { if (this.document.uri.toString() === data.notebook.toString()) { - document.detachCellTextDocument(textDocument); + document.detachCellTextDocument(documentData); } } } @@ -593,6 +597,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _documents = new Map(); private readonly _editors = new Map }>(); private readonly _notebookOutputRenderers = new Map(); + + private readonly _onDidChangeNotebookDocument = new Emitter<{ document: ExtHostNotebookDocument, changes: NotebookCellsSplice2[] }>(); + readonly onDidChangeNotebookDocument: Event<{ document: ExtHostNotebookDocument, changes: NotebookCellsSplice2[] }> = this._onDidChangeNotebookDocument.event; + private _outputDisplayOrder: INotebookDisplayOrder | undefined; get outputDisplayOrder(): INotebookDisplayOrder | undefined { @@ -782,6 +790,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (editor) { editor.editor.document.accpetModelChanged(event); + this._onDidChangeNotebookDocument.fire({ + document: editor.editor.document, + changes: event.changes + }); } } diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts new file mode 100644 index 00000000000..60e87eca726 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as types from 'vs/workbench/api/common/extHostTypes'; +import * as vscode from 'vscode'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ExtHostNotebookDocument, ExtHostNotebookController, ExtHostCell } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { score } from 'vs/editor/common/modes/languageSelector'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { isEqual } from 'vs/base/common/resources'; + +export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextDocument { + + private _disposables = new DisposableStore(); + + private _cells!: ExtHostCell[]; + private _cellLengths!: PrefixSumComputer; + private _cellLines!: PrefixSumComputer; + private _versionId = 0; + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + constructor( + extHostNotebooks: ExtHostNotebookController, + extHostDocuments: ExtHostDocuments, + private readonly _notebook: ExtHostNotebookDocument, + private readonly _selector: vscode.DocumentSelector | undefined, + ) { + this._init(); + + this._disposables.add(extHostDocuments.onDidChangeDocument(e => { + let cellIdx = this._cells.findIndex(cell => isEqual(cell.uri, e.document.uri)); + if (cellIdx >= 0) { + this._cellLengths.changeValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); + this._cellLines.changeValue(cellIdx, this._cells[cellIdx].document.lineCount); + this._versionId += 1; + this._onDidChange.fire(undefined); + } + })); + this._disposables.add(extHostNotebooks.onDidChangeNotebookDocument(e => { + if (e.document === this._notebook) { + this._init(); + this._versionId += 1; + this._onDidChange.fire(undefined); + } + })); + } + + dispose(): void { + this._disposables.dispose(); + } + + private _init() { + this._cells = []; + const cellLengths: number[] = []; + const cellLineCounts: number[] = []; + for (let cell of this._notebook.cells) { + if (cell.cellKind === CellKind.Code && (!this._selector || score(this._selector, cell.uri, cell.language, true))) { + this._cells.push(cell); + cellLengths.push(cell.document.getText().length + 1); + cellLineCounts.push(cell.document.lineCount); + } + } + this._cellLengths = new PrefixSumComputer(new Uint32Array(cellLengths)); + this._cellLines = new PrefixSumComputer(new Uint32Array(cellLineCounts)); + } + + get version(): number { + return this._versionId; + } + + getText(range?: vscode.Range): string { + if (!range) { + let result = ''; + for (let cell of this._cells) { + result += cell.document.getText() + '\n'; + } + // remove last newline again + result = result.slice(0, -1); + return result; + } + + if (range.isEmpty) { + return ''; + } + + // get start and end locations and create substrings + const start = this.locationAt(range.start); + const end = this.locationAt(range.end); + const startCell = this._cells.find(cell => isEqual(cell.uri, start.uri)); + const endCell = this._cells.find(cell => isEqual(cell.uri, end.uri)); + + if (!startCell || !endCell) { + return ''; + } else if (startCell === endCell) { + return startCell.document.getText(new types.Range(start.range.start, end.range.end)); + } else { + let a = startCell.document.getText(new types.Range(start.range.start, new types.Position(startCell.document.lineCount, 0))); + let b = endCell.document.getText(new types.Range(new types.Position(0, 0), end.range.end)); + return a + '\n' + b; + } + } + + offsetAt(position: vscode.Position): number { + const idx = this._cellLines.getIndexOf(position.line); + const offset1 = this._cellLengths.getAccumulatedValue(idx.index - 1); + const offset2 = this._cells[idx.index].document.offsetAt(position.with(idx.remainder)); + return offset1 + offset2; + } + + positionAt(locationOrOffset: vscode.Location | number): vscode.Position { + if (typeof locationOrOffset === 'number') { + const idx = this._cellLengths.getIndexOf(locationOrOffset); + const lineCount = this._cellLines.getAccumulatedValue(idx.index - 1); + return this._cells[idx.index].document.positionAt(idx.remainder).translate(lineCount); + } + + const idx = this._cells.findIndex(cell => isEqual(cell.uri, locationOrOffset.uri)); + if (idx >= 0) { + let line = this._cellLines.getAccumulatedValue(idx - 1); + return new types.Position(line + locationOrOffset.range.start.line, locationOrOffset.range.start.character); + } + // do better? + // return undefined; + return new types.Position(0, 0); + } + + locationAt(positionOrRange: vscode.Range | vscode.Position): types.Location { + if (!types.Range.isRange(positionOrRange)) { + positionOrRange = new types.Range(positionOrRange, positionOrRange); + } + + const startIdx = this._cellLines.getIndexOf(positionOrRange.start.line); + let endIdx = startIdx; + if (!positionOrRange.isEmpty) { + endIdx = this._cellLines.getIndexOf(positionOrRange.end.line); + } + + let startPos = new types.Position(startIdx.remainder, positionOrRange.start.character); + let endPos = new types.Position(endIdx.remainder, positionOrRange.end.character); + let range = new types.Range(startPos, endPos); + + const startCell = this._cells[startIdx.index]; + return new types.Location(startCell.uri, startCell.document.validateRange(range)); + } +} diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 7cc07cc6f04..e069ca76581 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -489,9 +489,9 @@ export class ResetViewLocationsAction extends Action { async run(): Promise { const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); viewContainerRegistry.all.forEach(viewContainer => { - const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); - viewDescriptors.allViewDescriptors.forEach(viewDescriptor => { + viewContainerModel.allViewDescriptors.forEach(viewDescriptor => { const defaultContainer = this.viewDescriptorService.getDefaultContainer(viewDescriptor.id); const currentContainer = this.viewDescriptorService.getViewContainer(viewDescriptor.id); @@ -584,7 +584,7 @@ export class MoveFocusedViewAction extends Action { const currentContainer = this.viewDescriptorService.getViewContainer(focusedViewId)!; const currentLocation = this.viewDescriptorService.getViewLocation(focusedViewId)!; - const isViewSolo = this.viewDescriptorService.getViewDescriptors(currentContainer).allViewDescriptors.length === 1; + const isViewSolo = this.viewDescriptorService.getViewContainerModel(currentContainer).allViewDescriptors.length === 1; if (!(isViewSolo && currentLocation === ViewContainerLocation.Sidebar)) { items.push({ diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index fa197206bb4..eabd38981aa 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -206,12 +206,7 @@ export class WorkbenchContextKeysHandler extends Disposable { if (activeEditorPane) { this.activeEditorContext.set(activeEditorPane.getId()); - try { - this.activeEditorIsReadonly.set(activeEditorPane.input.isReadonly()); - } catch (error) { - // TODO@ben for https://github.com/microsoft/vscode/issues/93224 - throw new Error(`${error.message}: editor id ${activeEditorPane.getId()}`); - } + this.activeEditorIsReadonly.set(activeEditorPane.input.isReadonly()); } else { this.activeEditorContext.reset(); this.activeEditorIsReadonly.reset(); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 5afb9bf1931..bd0ff9de084 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -30,6 +30,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Codicon } from 'vs/base/common/codicons'; +import { isString } from 'vs/base/common/types'; export class ViewletActivityAction extends ActivityAction { @@ -251,13 +252,17 @@ export class PlaceHolderViewletActivityAction extends ViewletActivityAction { constructor( id: string, - name: string, - iconUrl: URI | undefined, + icon: URI | string | undefined, @IViewletService viewletService: IViewletService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ITelemetryService telemetryService: ITelemetryService ) { - super({ id, name: id, iconUrl }, viewletService, layoutService, telemetryService); + super({ + id, + name: id, + iconUrl: URI.isUri(icon) ? icon : undefined, + cssClass: isString(icon) ? icon : undefined + }, viewletService, layoutService, telemetryService); } } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 52fc869b7d1..a2e00ec6a5c 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -26,7 +26,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { URI, UriComponents } from 'vs/base/common/uri'; import { ToggleCompositePinnedAction, ICompositeBarColors, ActivityAction, ICompositeActivity } from 'vs/workbench/browser/parts/compositeBarActions'; import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; -import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewDescriptorCollection, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewContainerModel, ViewContainerLocation } from 'vs/workbench/common/views'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { isUndefinedOrNull, assertIsDefined, isString } from 'vs/base/common/types'; @@ -48,6 +48,7 @@ interface IPlaceholderViewlet { id: string; name?: string; iconUrl?: UriComponents; + iconCSS?: string; views?: { when?: string }[]; } @@ -61,7 +62,7 @@ interface IPinnedViewlet { interface ICachedViewlet { id: string; name?: string; - iconUrl?: UriComponents; + icon?: URI | string; pinned: boolean; order?: number; visible: boolean; @@ -199,7 +200,6 @@ export class ActivitybarPart extends Part implements IActivityBarService { private onDidRegisterExtensions(): void { this.removeNotExistingComposites(); - this.saveCachedViewlets(); } @@ -217,8 +217,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (viewletDescriptor) { const viewContainer = this.getViewContainer(viewletDescriptor.id); if (viewContainer?.hideIfEmpty) { - const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); - if (viewDescriptors.activeViewDescriptors.length === 0) { + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + if (viewContainerModel.activeViewDescriptors.length === 0) { this.hideComposite(viewletDescriptor.id); // Update the composite bar by hiding } } @@ -450,7 +450,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { } else { const cachedComposite = this.cachedViewlets.filter(c => c.id === compositeId)[0]; compositeActions = { - activityAction: this.instantiationService.createInstance(PlaceHolderViewletActivityAction, compositeId, cachedComposite?.name || compositeId, cachedComposite?.iconUrl ? URI.revive(cachedComposite.iconUrl) : undefined), + activityAction: this.instantiationService.createInstance(PlaceHolderViewletActivityAction, compositeId, cachedComposite?.icon), pinnedAction: new PlaceHolderToggleCompositePinnedAction(compositeId, this.compositeBar) }; } @@ -482,11 +482,16 @@ export class ActivitybarPart extends Part implements IActivityBarService { } for (const viewlet of viewlets) { - this.enableCompositeActions(viewlet); const viewContainer = this.getViewContainer(viewlet.id)!; - const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); - this.onDidChangeActiveViews(viewlet, viewDescriptors, viewContainer.hideIfEmpty); - this.viewletDisposables.set(viewlet.id, viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(viewlet, viewDescriptors, viewContainer.hideIfEmpty))); + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + this.updateActivity(viewContainer, viewContainerModel); + this.onDidChangeActiveViews(viewContainer, viewContainerModel); + + const disposables = new DisposableStore(); + disposables.add(viewContainerModel.onDidChangeContainerInfo(() => this.updateActivity(viewContainer, viewContainerModel))); + disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.onDidChangeActiveViews(viewContainer, viewContainerModel))); + + this.viewletDisposables.set(viewlet.id, disposables); } } @@ -500,34 +505,31 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.hideComposite(viewletId); } - private updateActivity(viewlet: ViewletDescriptor, viewDescriptors: IViewDescriptorCollection): void { - const viewDescriptor = viewDescriptors.activeViewDescriptors[0]; - - // Use the viewlet icon if any view inside belongs to it statically - const shouldUseViewletIcon = viewDescriptors.allViewDescriptors.some(v => this.viewDescriptorService.getDefaultContainer(v.id)?.id === viewlet.id); + private updateActivity(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { const activity: IActivity = { - id: viewlet.id, - name: shouldUseViewletIcon ? viewlet.name : viewDescriptor.name, - cssClass: shouldUseViewletIcon ? viewlet.cssClass : (isString(viewDescriptor.containerIcon) ? viewDescriptor.containerIcon : (viewDescriptor.containerIcon === undefined ? Codicon.window.classNames : undefined)), - iconUrl: shouldUseViewletIcon ? viewlet.iconUrl : (viewDescriptor.containerIcon instanceof URI ? viewDescriptor.containerIcon : undefined), - keybindingId: viewlet.keybindingId + id: viewContainer.id, + name: viewContainerModel.title, + iconUrl: URI.isUri(viewContainerModel.icon) ? viewContainerModel.icon : undefined, + cssClass: isString(viewContainerModel.icon) ? viewContainerModel.icon : undefined, + keybindingId: viewContainer.focusCommand?.id || viewContainer.id }; - const { activityAction, pinnedAction } = this.getCompositeActions(viewlet.id); + const { activityAction, pinnedAction } = this.getCompositeActions(viewContainer.id); activityAction.setActivity(activity); if (pinnedAction instanceof PlaceHolderToggleCompositePinnedAction) { pinnedAction.setActivity(activity); } + + this.saveCachedViewlets(); } - private onDidChangeActiveViews(viewlet: ViewletDescriptor, viewDescriptors: IViewDescriptorCollection, hideIfEmpty?: boolean): void { - if (viewDescriptors.activeViewDescriptors.length) { - this.updateActivity(viewlet, viewDescriptors); - this.compositeBar.addComposite(viewlet); - } else if (hideIfEmpty) { - this.hideComposite(viewlet.id); + private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { + if (viewContainerModel.activeViewDescriptors.length) { + this.compositeBar.addComposite(viewContainer); + } else if (viewContainer.hideIfEmpty) { + this.hideComposite(viewContainer.id); } } @@ -562,17 +564,6 @@ export class ActivitybarPart extends Part implements IActivityBarService { } } - private enableCompositeActions(viewlet: ViewletDescriptor): void { - const { activityAction, pinnedAction } = this.getCompositeActions(viewlet.id); - if (activityAction instanceof PlaceHolderViewletActivityAction) { - activityAction.setActivity(viewlet); - } - - if (pinnedAction instanceof PlaceHolderToggleCompositePinnedAction) { - pinnedAction.setActivity(viewlet); - } - } - getPinnedViewletIds(): string[] { const pinnedCompositeIds = this.compositeBar.getPinnedComposites().map(v => v.id); @@ -641,21 +632,25 @@ export class ActivitybarPart extends Part implements IActivityBarService { private saveCachedViewlets(): void { const state: ICachedViewlet[] = []; - const allViewlets = this.viewletService.getViewlets(); const compositeItems = this.compositeBar.getCompositeBarItems(); for (const compositeItem of compositeItems) { const viewContainer = this.getViewContainer(compositeItem.id); - const viewlet = allViewlets.filter(({ id }) => id === compositeItem.id)[0]; - if (viewlet) { + if (viewContainer) { + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); const views: { when: string | undefined }[] = []; - if (viewContainer) { - const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); - for (const { when } of viewDescriptors.allViewDescriptors) { - views.push({ when: when ? when.serialize() : undefined }); - } + for (const { when } of viewContainerModel.allViewDescriptors) { + views.push({ when: when ? when.serialize() : undefined }); } - state.push({ id: compositeItem.id, name: viewlet.name, iconUrl: viewlet.iconUrl && viewlet.iconUrl.scheme === Schemas.file ? viewlet.iconUrl : undefined, views, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); + state.push({ + id: compositeItem.id, + name: viewContainerModel.title, + icon: URI.isUri(viewContainerModel.icon) && viewContainerModel.icon.scheme === Schemas.file ? viewContainerModel.icon : viewContainerModel.icon, + views, + pinned: compositeItem.pinned, + order: compositeItem.order, + visible: compositeItem.visible + }); } else { state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false }); } @@ -665,12 +660,13 @@ export class ActivitybarPart extends Part implements IActivityBarService { } private getCachedViewlets(): ICachedViewlet[] { - const cachedViewlets: Array = JSON.parse(this.pinnedViewletsValue); - for (const placeholderViewlet of JSON.parse(this.placeholderViewletsValue)) { + const cachedViewlets: ICachedViewlet[] = this.getPinnedViewlets(); + for (const placeholderViewlet of this.getPlaceholderViewlets()) { const cachedViewlet = cachedViewlets.filter(cached => cached.id === placeholderViewlet.id)[0]; if (cachedViewlet) { cachedViewlet.name = placeholderViewlet.name; - cachedViewlet.iconUrl = placeholderViewlet.iconUrl; + cachedViewlet.icon = placeholderViewlet.iconCSS ? placeholderViewlet.iconCSS : + placeholderViewlet.iconUrl ? URI.revive(placeholderViewlet.iconUrl) : undefined; cachedViewlet.views = placeholderViewlet.views; } } @@ -679,8 +675,27 @@ export class ActivitybarPart extends Part implements IActivityBarService { } private storeCachedViewletsState(cachedViewlets: ICachedViewlet[]): void { - this.pinnedViewletsValue = JSON.stringify(cachedViewlets.map(({ id, pinned, visible, order }) => ({ id, pinned, visible, order }))); - this.placeholderViewletsValue = JSON.stringify(cachedViewlets.map(({ id, iconUrl, name, views }) => ({ id, iconUrl, name, views }))); + this.setPinnedViewlets(cachedViewlets.map(({ id, pinned, visible, order }) => ({ + id, + pinned, + visible, + order + }))); + this.setPlaceholderViewlets(cachedViewlets.map(({ id, icon, name, views }) => ({ + id, + iconUrl: URI.isUri(icon) ? icon : undefined, + iconCSS: isString(icon) ? icon : undefined, + name, + views + }))); + } + + private getPinnedViewlets(): IPinnedViewlet[] { + return JSON.parse(this.pinnedViewletsValue); + } + + private setPinnedViewlets(pinnedViewlets: IPinnedViewlet[]): void { + this.pinnedViewletsValue = JSON.stringify(pinnedViewlets); } private _pinnedViewletsValue: string | undefined; @@ -707,6 +722,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.storageService.store(ActivitybarPart.PINNED_VIEWLETS, value, StorageScope.GLOBAL); } + private getPlaceholderViewlets(): IPlaceholderViewlet[] { + return JSON.parse(this.placeholderViewletsValue); + } + + private setPlaceholderViewlets(placeholderViewlets: IPlaceholderViewlet[]): void { + this.placeholderViewletsValue = JSON.stringify(placeholderViewlets); + } + private _placeholderViewletsValue: string | undefined; private get placeholderViewletsValue(): string { if (!this._placeholderViewletsValue) { @@ -736,7 +759,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (value !== undefined) { const storedStates: Array = JSON.parse(value); const cachedViewlets = storedStates.map(c => { - const serialized: ICachedViewlet = typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true, order: undefined, visible: true, name: undefined, iconUrl: undefined, views: undefined } : c; + const serialized: ICachedViewlet = typeof c === 'string' /* migration from pinned states to composites states */ ? { id: c, pinned: true, order: undefined, visible: true, name: undefined, icon: undefined, views: undefined } : c; serialized.visible = isUndefinedOrNull(serialized.visible) ? true : serialized.visible; return serialized; }); diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 2b67559e904..d25f80c50d7 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -56,7 +56,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } // ... on a different composite bar else { - const viewsToMove = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors; + const viewsToMove = this.viewDescriptorService.getViewContainerModel(currentContainer)!.allViewDescriptors; if (viewsToMove.some(v => !v.canMoveView)) { return; } @@ -119,7 +119,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } // ... to another composite location - const draggedViews = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors; + const draggedViews = this.viewDescriptorService.getViewContainerModel(currentContainer)!.allViewDescriptors; // ... all views must be movable return !draggedViews.some(v => !v.canMoveView); diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 0462c2b2a8a..68228296c84 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -70,6 +70,7 @@ export class ActivityAction extends Action { } set activity(activity: IActivity) { + this._label = activity.name; this._activity = activity; this._onDidChangeActivity.fire(this); } @@ -162,9 +163,11 @@ export class ActivityActionViewItem extends BaseActionViewItem { if (this.activity.iconUrl) { // Apply background color to activity bar item provided with iconUrls this.label.style.backgroundColor = foreground ? foreground.toString() : ''; + this.label.style.color = ''; } else { // Apply foreground color to activity bar items provided with codicons this.label.style.color = foreground ? foreground.toString() : ''; + this.label.style.backgroundColor = ''; } const dragColor = colors.activeBackgroundColor || colors.activeForegroundColor; @@ -692,9 +695,4 @@ export class ToggleCompositePinnedAction extends Action { this.compositeBar.pin(id); } } - - setActivity(activity: IActivity): void { - this.activity = activity; - this.label = activity.name; - } } diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index e57afd9b033..d6fb1701dbb 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -107,6 +107,8 @@ export abstract class BaseEditor extends Composite implements IEditorPane { onHide() { } + onWillHide() { } + /** * Called to create the editor in the parent HTMLElement. */ diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index c7acfb330d2..6262b90a14a 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -205,6 +205,7 @@ export class EditorControl extends Disposable { // Remove editor pane from parent and hide const editorPaneContainer = this._activeEditorPane.getContainer(); if (editorPaneContainer) { + this._activeEditorPane.onWillHide(); this.parent.removeChild(editorPaneContainer); hide(editorPaneContainer); this._activeEditorPane.onHide(); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index d708c9e414f..ba081b013e1 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -831,6 +831,11 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro parent.appendChild(overlay); CompositeDragAndDropObserver.INSTANCE.registerTarget(this.element, { + onDragOver: e => { + if (e.eventData.dataTransfer) { + e.eventData.dataTransfer.dropEffect = 'none'; + } + }, onDragStart: e => { toggleClass(overlay, 'visible', true); }, diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 63666da73e2..1a9cc0229d1 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -205,6 +205,10 @@ export class PlaceHolderToggleCompositePinnedAction extends ToggleCompositePinne constructor(id: string, compositeBar: ICompositeBar) { super({ id, name: id, cssClass: undefined }, compositeBar); } + + setActivity(activity: IActivity): void { + this.label = activity.name; + } } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 9001b38a9e0..1ba0b10b40b 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -33,12 +33,13 @@ import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/con import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewDescriptorService, IViewDescriptorCollection, ViewContainerLocation } from 'vs/workbench/common/views'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewDescriptorService, IViewContainerModel, ViewContainerLocation } from 'vs/workbench/common/views'; import { MenuId } from 'vs/platform/actions/common/actions'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { Before2D } from 'vs/workbench/browser/dnd'; +import { IActivity } from 'vs/workbench/common/activity'; interface ICachedPanel { id: string; @@ -177,9 +178,9 @@ export class PanelPart extends CompositePart implements IPanelService { const result: IAction[] = []; const container = this.getViewContainer(compositeId); if (container) { - const viewDescriptors = this.viewDescriptorService.getViewDescriptors(container); - if (viewDescriptors.allViewDescriptors.length === 1) { - const viewMenuActions = this.instantiationService.createInstance(ViewMenuActions, viewDescriptors.allViewDescriptors[0].id, MenuId.ViewTitle, MenuId.ViewTitleContext); + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(container); + if (viewContainerModel.allViewDescriptors.length === 1) { + const viewMenuActions = this.instantiationService.createInstance(ViewMenuActions, viewContainerModel.allViewDescriptors[0].id, MenuId.ViewTitle, MenuId.ViewTitleContext); result.push(...viewMenuActions.getContextMenuActions()); viewMenuActions.dispose(); } @@ -208,13 +209,16 @@ export class PanelPart extends CompositePart implements IPanelService { } for (const panel of panels) { - this.enableCompositeActions(panel); - const viewContainer = this.getViewContainer(panel.id); - if (viewContainer?.hideIfEmpty) { - const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); - this.onDidChangeActiveViews(panel, viewDescriptors); - this.panelDisposables.set(panel.id, viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(panel, viewDescriptors))); - } + const viewContainer = this.getViewContainer(panel.id)!; + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + this.updateActivity(viewContainer, viewContainerModel); + this.onDidChangeActiveViews(viewContainer, viewContainerModel); + + const disposables = new DisposableStore(); + disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.onDidChangeActiveViews(viewContainer, viewContainerModel))); + disposables.add(viewContainerModel.onDidChangeContainerInfo(() => this.updateActivity(viewContainer, viewContainerModel))); + + this.panelDisposables.set(panel.id, disposables); } } @@ -228,22 +232,28 @@ export class PanelPart extends CompositePart implements IPanelService { this.hideComposite(panelId); } - private enableCompositeActions(panel: PanelDescriptor): void { - const { activityAction, pinnedAction } = this.getCompositeActions(panel.id); - if (activityAction instanceof PlaceHolderPanelActivityAction) { - activityAction.setActivity(panel); - } + private updateActivity(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { + const activity: IActivity = { + id: viewContainer.id, + name: viewContainerModel.title, + keybindingId: viewContainer.focusCommand?.id + }; + + const { activityAction, pinnedAction } = this.getCompositeActions(viewContainer.id); + activityAction.setActivity(activity); if (pinnedAction instanceof PlaceHolderToggleCompositePinnedAction) { - pinnedAction.setActivity(panel); + pinnedAction.setActivity(activity); } + + this.saveCachedPanels(); } - private onDidChangeActiveViews(panel: PanelDescriptor, viewDescriptors: IViewDescriptorCollection): void { - if (viewDescriptors.activeViewDescriptors.length) { - this.compositeBar.addComposite(panel); - } else { - this.hideComposite(panel.id); + private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { + if (viewContainerModel.activeViewDescriptors.length) { + this.compositeBar.addComposite(viewContainer); + } else if (viewContainer.hideIfEmpty) { + this.hideComposite(viewContainer.id); } } @@ -320,8 +330,8 @@ export class PanelPart extends CompositePart implements IPanelService { if (panelDescriptor) { const viewContainer = this.getViewContainer(panelDescriptor.id); if (viewContainer?.hideIfEmpty) { - const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); - if (viewDescriptors.activeViewDescriptors.length === 0 && this.compositeBar.getPinnedComposites().length > 1) { + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + if (viewContainerModel.activeViewDescriptors.length === 0 && this.compositeBar.getPinnedComposites().length > 1) { this.hideComposite(panelDescriptor.id); // Update the composite bar by hiding } } @@ -540,28 +550,6 @@ export class PanelPart extends CompositePart implements IPanelService { return false; } - protected onTitleAreaUpdate(compositeId: string): void { - super.onTitleAreaUpdate(compositeId); - - const activePanel = this.getActivePanel(); - const panel = this.createComposite(compositeId, activePanel?.getId() === compositeId); - - if (panel) { - const compositeActions = this.compositeActions.get(compositeId); - if (compositeActions) { - compositeActions.activityAction.setActivity({ - id: compositeActions.activityAction.id, - name: panel.getTitle() || compositeActions.activityAction.label - }); - - compositeActions.pinnedAction.setActivity({ - id: compositeActions.activityAction.id, - name: panel.getTitle() || compositeActions.activityAction.label - }); - } - } - } - private getToolbarWidth(): number { const activePanel = this.getActivePanel(); if (!activePanel || !this.toolBar) { @@ -609,8 +597,11 @@ export class PanelPart extends CompositePart implements IPanelService { const compositeItems = this.compositeBar.getCompositeBarItems(); for (const compositeItem of compositeItems) { - const activityAction = this.getCompositeActions(compositeItem.id).activityAction; - state.push({ id: compositeItem.id, name: activityAction.label, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); + const viewContainer = this.getViewContainer(compositeItem.id); + if (viewContainer) { + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + state.push({ id: compositeItem.id, name: viewContainerModel.title, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); + } } this.cachedPanelsValue = JSON.stringify(state); diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 7e771ce718f..16c7bcdd413 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -24,11 +24,10 @@ import { PaneView, IPaneViewOptions, IPaneOptions, Pane } from 'vs/base/browser/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; -import { PersistentContributableViewsModel, IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -746,7 +745,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { private readonly visibleViewsCountFromCache: number | undefined; private readonly visibleViewsStorageId: string; - protected readonly viewsModel: PersistentContributableViewsModel; + protected readonly viewContainerModel: IViewContainerModel; private viewDisposables: IDisposable[] = []; private readonly _onTitleAreaUpdate: Emitter = this._register(new Emitter()); @@ -782,7 +781,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { constructor( id: string, - viewPaneContainerStateStorageId: string, private options: IViewPaneContainerOptions, @IInstantiationService protected instantiationService: IInstantiationService, @IConfigurationService protected configurationService: IConfigurationService, @@ -808,7 +806,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.visibleViewsStorageId = `${id}.numberOfVisibleViews`; this.visibleViewsCountFromCache = this.storageService.getNumber(this.visibleViewsStorageId, StorageScope.WORKSPACE, undefined); this._register(toDisposable(() => this.viewDisposables = dispose(this.viewDisposables))); - this.viewsModel = this._register(this.instantiationService.createInstance(PersistentContributableViewsModel, container, viewPaneContainerStateStorageId)); + this.viewContainerModel = this.viewDescriptorService.getViewContainerModel(container); } create(parent: HTMLElement): void { @@ -839,7 +837,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const container = viewContainerRegistry.get(dropData.id)!; - const viewsToMove = this.viewDescriptorService.getViewDescriptors(container).allViewDescriptors; + const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors; if (!viewsToMove.some(v => !v.canMoveView)) { overlay = new ViewPaneDropOverlay(parent, undefined, this.themeService); @@ -861,7 +859,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const container = viewContainerRegistry.get(dropData.id)!; - const allViews = this.viewDescriptorService.getViewDescriptors(container).allViewDescriptors; + const allViews = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors; if (!allViews.some(v => !v.canMoveView)) { viewsToMove.push(...allViews); } @@ -884,11 +882,11 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { })); this._register(this.onDidSashChange(() => this.saveViewSizes())); - this.viewsModel.onDidAdd(added => this.onDidAddViewDescriptors(added)); - this.viewsModel.onDidRemove(removed => this.onDidRemoveViewDescriptors(removed)); - const addedViews: IAddedViewDescriptorRef[] = this.viewsModel.visibleViewDescriptors.map((viewDescriptor, index) => { - const size = this.viewsModel.getSize(viewDescriptor.id); - const collapsed = this.viewsModel.isCollapsed(viewDescriptor.id); + this.viewContainerModel.onDidAddVisibleViewDescriptors(added => this.onDidAddViewDescriptors(added)); + this.viewContainerModel.onDidRemoveVisibleViewDescriptors(removed => this.onDidRemoveViewDescriptors(removed)); + const addedViews: IAddedViewDescriptorRef[] = this.viewContainerModel.visibleViewDescriptors.map((viewDescriptor, index) => { + const size = this.viewContainerModel.getSize(viewDescriptor.id); + const collapsed = this.viewContainerModel.isCollapsed(viewDescriptor.id); return ({ viewDescriptor, index, size, collapsed }); }); if (addedViews.length) { @@ -906,9 +904,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } getTitle(): string { - // use registered title if any of our panes are statically registered to this container - const allViewDescriptors = this.viewDescriptorService.getViewDescriptors(this.viewContainer).allViewDescriptors; - const containerTitle = this.paneItems.length === 0 || allViewDescriptors.length === 0 || allViewDescriptors.some(v => this.viewDescriptorService.getDefaultContainer(v.id) === this.viewContainer) ? this.viewContainer.name : this.paneItems[0].pane.title; + const containerTitle = this.viewContainerModel.title; if (this.isViewMergedWithContainer()) { const paneItemTitle = this.paneItems[0].pane.title; @@ -959,10 +955,10 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - const viewToggleActions = this.viewsModel.viewDescriptors.map(viewDescriptor => ({ + const viewToggleActions = this.viewContainerModel.activeViewDescriptors.map(viewDescriptor => ({ id: `${viewDescriptor.id}.toggleVisibility`, label: viewDescriptor.name, - checked: this.viewsModel.isVisible(viewDescriptor.id), + checked: this.viewContainerModel.isVisible(viewDescriptor.id), enabled: viewDescriptor.canToggleVisibility, run: () => this.toggleViewVisibility(viewDescriptor.id) })); @@ -1091,7 +1087,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { // Save size only when the layout has happened if (this.didLayout) { for (const view of this.panes) { - this.viewsModel.setSize(view.id, this.getPaneSize(view)); + this.viewContainerModel.setSize(view.id, this.getPaneSize(view)); } } } @@ -1100,10 +1096,10 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { // Restore sizes only when the layout has happened if (this.didLayout) { let initialSizes; - for (let i = 0; i < this.viewsModel.visibleViewDescriptors.length; i++) { + for (let i = 0; i < this.viewContainerModel.visibleViewDescriptors.length; i++) { const pane = this.panes[i]; - const viewDescriptor = this.viewsModel.visibleViewDescriptors[i]; - const size = this.viewsModel.getSize(viewDescriptor.id); + const viewDescriptor = this.viewContainerModel.visibleViewDescriptors[i]; + const size = this.viewContainerModel.getSize(viewDescriptor.id); if (typeof size === 'number') { this.resizePane(pane, size); @@ -1118,8 +1114,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { private computeInitialSizes(): Map { const sizes: Map = new Map(); if (this.dimension) { - const totalWeight = this.viewsModel.visibleViewDescriptors.reduce((totalWeight, { weight }) => totalWeight + (weight || 20), 0); - for (const viewDescriptor of this.viewsModel.visibleViewDescriptors) { + const totalWeight = this.viewContainerModel.visibleViewDescriptors.reduce((totalWeight, { weight }) => totalWeight + (weight || 20), 0); + for (const viewDescriptor of this.viewContainerModel.visibleViewDescriptors) { if (this.orientation === Orientation.VERTICAL) { sizes.set(viewDescriptor.id, this.dimension.height * (viewDescriptor.weight || 20) / totalWeight); } else { @@ -1180,7 +1176,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { }); const collapseDisposable = Event.latch(Event.map(pane.onDidChange, () => !pane.isExpanded()))(collapsed => { - this.viewsModel.setCollapsed(viewDescriptor.id, collapsed); + this.viewContainerModel.setCollapsed(viewDescriptor.id, collapsed); }); this.viewDisposables.splice(index, 0, combinedDisposable(contextMenuDisposable, collapseDisposable)); @@ -1211,13 +1207,13 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } protected toggleViewVisibility(viewId: string): void { - const visible = !this.viewsModel.isVisible(viewId); + const visible = !this.viewContainerModel.isVisible(viewId); type ViewsToggleVisibilityClassification = { viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; visible: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; this.telemetryService.publicLog2<{ viewId: String, visible: boolean }, ViewsToggleVisibilityClassification>('views.toggleVisibility', { viewId, visible }); - this.viewsModel.setVisible(viewId, visible); + this.viewContainerModel.setVisible(viewId, visible); } private addPane(pane: ViewPane, size: number, index = this.paneItems.length - 1): void { @@ -1272,7 +1268,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const container = viewContainerRegistry.get(dropData.id)!; - const viewsToMove = this.viewDescriptorService.getViewDescriptors(container).allViewDescriptors; + const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors; if (!viewsToMove.some(v => !v.canMoveView)) { overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, this.themeService); @@ -1294,7 +1290,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const container = viewContainerRegistry.get(dropData.id)!; - const allViews = this.viewDescriptorService.getViewDescriptors(container).allViewDescriptors; + const allViews = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors; if (allViews.length > 0 && !allViews.some(v => !v.canMoveView)) { viewsToMove.push(...allViews); @@ -1409,8 +1405,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const fromIndex = firstIndex(this.paneItems, item => item.pane === from); const toIndex = firstIndex(this.paneItems, item => item.pane === to); - const fromViewDescriptor = this.viewsModel.visibleViewDescriptors[fromIndex]; - const toViewDescriptor = this.viewsModel.visibleViewDescriptors[toIndex]; + const fromViewDescriptor = this.viewContainerModel.visibleViewDescriptors[fromIndex]; + const toViewDescriptor = this.viewContainerModel.visibleViewDescriptors[toIndex]; if (fromIndex < 0 || fromIndex >= this.paneItems.length) { return; @@ -1425,7 +1421,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { assertIsDefined(this.paneview).movePane(from, to); - this.viewsModel.move(fromViewDescriptor.id, toViewDescriptor.id); + this.viewContainerModel.move(fromViewDescriptor.id, toViewDescriptor.id); this.updateTitleArea(); } diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 42ae7b7deda..d9e0c6e66a3 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -7,16 +7,14 @@ import 'vs/css!./media/views'; import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IViewDescriptorService, ViewContainer, IViewDescriptor, IViewContainersRegistry, Extensions as ViewExtensions, IView, ViewContainerLocation, IViewsService, IViewPaneContainer, getVisbileViewContextKey } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; -import { firstIndex, move } from 'vs/base/common/arrays'; -import { isUndefinedOrNull, isUndefined, isString } from 'vs/base/common/types'; +import { isString } from 'vs/base/common/types'; import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { localize } from 'vs/nls'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { values } from 'vs/base/common/map'; import { toggleClass, addClass } from 'vs/base/browser/dom'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; @@ -34,488 +32,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { URI } from 'vs/base/common/uri'; import { IProgressIndicator } from 'vs/platform/progress/common/progress'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; - -export interface IViewState { - visibleGlobal: boolean | undefined; - visibleWorkspace: boolean | undefined; - collapsed: boolean | undefined; - order?: number; - size?: number; -} - -export interface IViewDescriptorRef { - viewDescriptor: IViewDescriptor; - index: number; -} - -export interface IAddedViewDescriptorRef extends IViewDescriptorRef { - collapsed: boolean; - size?: number; -} - -export class ContributableViewsModel extends Disposable { - - private _viewDescriptors: IViewDescriptor[] = []; - get viewDescriptors(): ReadonlyArray { - return this._viewDescriptors; - } - - get visibleViewDescriptors(): IViewDescriptor[] { - return this.viewDescriptors.filter(v => this.isViewDescriptorVisible(v)); - } - - private _onDidAdd = this._register(new Emitter()); - readonly onDidAdd: Event = this._onDidAdd.event; - - private _onDidRemove = this._register(new Emitter()); - readonly onDidRemove: Event = this._onDidRemove.event; - - private _onDidMove = this._register(new Emitter<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }>()); - readonly onDidMove: Event<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }> = this._onDidMove.event; - - private _onDidChangeViewState = this._register(new Emitter()); - protected readonly onDidChangeViewState: Event = this._onDidChangeViewState.event; - - private _onDidChangeActiveViews = this._register(new Emitter>()); - readonly onDidChangeActiveViews: Event> = this._onDidChangeActiveViews.event; - - constructor( - container: ViewContainer, - viewsService: IViewDescriptorService, - protected viewStates = new Map(), - ) { - super(); - const viewDescriptorCollection = viewsService.getViewDescriptors(container); - this._register(viewDescriptorCollection.onDidChangeActiveViews(() => this.onDidChangeViewDescriptors(viewDescriptorCollection.activeViewDescriptors))); - this.onDidChangeViewDescriptors(viewDescriptorCollection.activeViewDescriptors); - } - - isVisible(id: string): boolean { - const viewDescriptor = this.viewDescriptors.filter(v => v.id === id)[0]; - - if (!viewDescriptor) { - throw new Error(`Unknown view ${id}`); - } - - return this.isViewDescriptorVisible(viewDescriptor); - } - - setVisible(id: string, visible: boolean, size?: number): void { - this.doSetVisible([{ id, visible, size }]); - } - - protected doSetVisible(viewDescriptors: { id: string, visible: boolean, size?: number }[]): void { - const added: IAddedViewDescriptorRef[] = []; - const removed: IViewDescriptorRef[] = []; - - for (const { visibleIndex, viewDescriptor, state, visible, size } of viewDescriptors.map(({ id, visible, size }) => ({ ...this.find(id), visible, size }))) { - - if (!viewDescriptor.canToggleVisibility) { - throw new Error(`Can't toggle this view's visibility`); - } - - if (this.isViewDescriptorVisible(viewDescriptor) === visible) { - return; - } - - if (viewDescriptor.workspace) { - state.visibleWorkspace = visible; - } else { - state.visibleGlobal = visible; - } - - if (typeof size === 'number') { - state.size = size; - } - - if (visible) { - added.push({ index: visibleIndex, viewDescriptor, size: state.size, collapsed: !!state.collapsed }); - } else { - removed.push({ index: visibleIndex, viewDescriptor }); - } - } - - if (added.length) { - this._onDidAdd.fire(added); - } - if (removed.length) { - this._onDidRemove.fire(removed); - } - } - - isCollapsed(id: string): boolean { - const state = this.viewStates.get(id); - - if (!state) { - throw new Error(`Unknown view ${id}`); - } - - return !!state.collapsed; - } - - setCollapsed(id: string, collapsed: boolean): void { - const { index, state, viewDescriptor } = this.find(id); - if (state.collapsed !== collapsed) { - state.collapsed = collapsed; - this._onDidChangeViewState.fire({ viewDescriptor, index }); - } - } - - getSize(id: string): number | undefined { - const state = this.viewStates.get(id); - - if (!state) { - throw new Error(`Unknown view ${id}`); - } - - return state.size; - } - - setSize(id: string, size: number): void { - const { index, state, viewDescriptor } = this.find(id); - if (state.size !== size) { - state.size = size; - this._onDidChangeViewState.fire({ viewDescriptor, index }); - } - } - - move(from: string, to: string): void { - const fromIndex = firstIndex(this.viewDescriptors, v => v.id === from); - const toIndex = firstIndex(this.viewDescriptors, v => v.id === to); - - const fromViewDescriptor = this.viewDescriptors[fromIndex]; - const toViewDescriptor = this.viewDescriptors[toIndex]; - - move(this._viewDescriptors, fromIndex, toIndex); - - for (let index = 0; index < this.viewDescriptors.length; index++) { - const state = this.viewStates.get(this.viewDescriptors[index].id)!; - state.order = index; - } - - this._onDidMove.fire({ - from: { index: fromIndex, viewDescriptor: fromViewDescriptor }, - to: { index: toIndex, viewDescriptor: toViewDescriptor } - }); - } - - private isViewDescriptorVisible(viewDescriptor: IViewDescriptor): boolean { - const viewState = this.viewStates.get(viewDescriptor.id); - if (!viewState) { - throw new Error(`Unknown view ${viewDescriptor.id}`); - } - return viewDescriptor.workspace ? !!viewState.visibleWorkspace : !!viewState.visibleGlobal; - } - - private find(id: string): { index: number, visibleIndex: number, viewDescriptor: IViewDescriptor, state: IViewState; } { - for (let i = 0, visibleIndex = 0; i < this.viewDescriptors.length; i++) { - const viewDescriptor = this.viewDescriptors[i]; - const state = this.viewStates.get(viewDescriptor.id); - if (!state) { - throw new Error(`View state for ${id} not found`); - } - - if (viewDescriptor.id === id) { - return { index: i, visibleIndex, viewDescriptor, state }; - } - - if (viewDescriptor.workspace ? state.visibleWorkspace : state.visibleGlobal) { - visibleIndex++; - } - } - - throw new Error(`view descriptor ${id} not found`); - } - - private compareViewDescriptors(a: IViewDescriptor, b: IViewDescriptor): number { - if (a.id === b.id) { - return 0; - } - - return (this.getViewOrder(a) - this.getViewOrder(b)) || this.getGroupOrderResult(a, b); - } - - private getGroupOrderResult(a: IViewDescriptor, b: IViewDescriptor) { - if (!a.group || !b.group) { - return 0; - } - - if (a.group === b.group) { - return 0; - } - - return a.group < b.group ? -1 : 1; - } - - private getViewOrder(viewDescriptor: IViewDescriptor): number { - const viewState = this.viewStates.get(viewDescriptor.id); - const viewOrder = viewState && typeof viewState.order === 'number' ? viewState.order : viewDescriptor.order; - return typeof viewOrder === 'number' ? viewOrder : Number.MAX_VALUE; - } - - private onDidChangeViewDescriptors(viewDescriptors: IViewDescriptor[]): void { - for (const viewDescriptor of viewDescriptors) { - const viewState = this.viewStates.get(viewDescriptor.id); - if (viewState) { - // set defaults if not set - if (viewDescriptor.workspace) { - viewState.visibleWorkspace = isUndefinedOrNull(viewState.visibleWorkspace) ? !viewDescriptor.hideByDefault : viewState.visibleWorkspace; - } else { - viewState.visibleGlobal = isUndefinedOrNull(viewState.visibleGlobal) ? !viewDescriptor.hideByDefault : viewState.visibleGlobal; - } - viewState.collapsed = isUndefinedOrNull(viewState.collapsed) ? !!viewDescriptor.collapsed : viewState.collapsed; - } else { - this.viewStates.set(viewDescriptor.id, { - visibleGlobal: !viewDescriptor.hideByDefault, - visibleWorkspace: !viewDescriptor.hideByDefault, - collapsed: !!viewDescriptor.collapsed - }); - } - } - - viewDescriptors = viewDescriptors.sort(this.compareViewDescriptors.bind(this)); - - const toRemove: { index: number, viewDescriptor: IViewDescriptor; }[] = []; - for (let index = 0; index < this._viewDescriptors.length; index++) { - const previousViewDescriptor = this._viewDescriptors[index]; - if (this.isViewDescriptorVisible(previousViewDescriptor) && viewDescriptors.every(viewDescriptor => viewDescriptor.id !== previousViewDescriptor.id)) { - const { visibleIndex } = this.find(previousViewDescriptor.id); - toRemove.push({ index: visibleIndex, viewDescriptor: previousViewDescriptor }); - } - } - - const previous = this._viewDescriptors; - this._viewDescriptors = viewDescriptors.slice(0); - - const toAdd: { index: number, viewDescriptor: IViewDescriptor, size?: number, collapsed: boolean; }[] = []; - for (let i = 0; i < this._viewDescriptors.length; i++) { - const viewDescriptor = this._viewDescriptors[i]; - if (this.isViewDescriptorVisible(viewDescriptor) && previous.every(previousViewDescriptor => previousViewDescriptor.id !== viewDescriptor.id)) { - const { visibleIndex, state } = this.find(viewDescriptor.id); - toAdd.push({ index: visibleIndex, viewDescriptor, size: state.size, collapsed: !!state.collapsed }); - } - } - - if (toRemove.length) { - this._onDidRemove.fire(toRemove); - } - - if (toAdd.length) { - this._onDidAdd.fire(toAdd); - } - - this._onDidChangeActiveViews.fire(this.viewDescriptors); - } -} - -interface IStoredWorkspaceViewState { - collapsed: boolean; - isHidden: boolean; - size?: number; - order?: number; -} - -interface IStoredGlobalViewState { - id: string; - isHidden: boolean; - order?: number; -} - -export class PersistentContributableViewsModel extends ContributableViewsModel { - - private readonly workspaceViewsStateStorageId: string; - private readonly globalViewsStateStorageId: string; - - private storageService: IStorageService; - - constructor( - container: ViewContainer, - viewletStateStorageId: string, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService - ) { - const globalViewsStateStorageId = `${viewletStateStorageId}.hidden`; - storageKeysSyncRegistryService.registerStorageKey({ key: globalViewsStateStorageId, version: 1 }); - const viewStates = PersistentContributableViewsModel.loadViewsStates(viewletStateStorageId, globalViewsStateStorageId, storageService); - - super(container, viewDescriptorService, viewStates); - - this.storageService = storageService; - this.workspaceViewsStateStorageId = viewletStateStorageId; - this.globalViewsStateStorageId = globalViewsStateStorageId; - - this._register(Event.any( - this.onDidAdd, - this.onDidRemove, - Event.map(this.onDidMove, ({ from, to }) => [from, to]), - Event.map(this.onDidChangeViewState, viewDescriptorRef => [viewDescriptorRef])) - (viewDescriptorRefs => this.saveViewsStates())); - - this._globalViewsStatesValue = this.getStoredGlobalViewsStatesValue(); - this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); - } - - private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { - if (e.key === this.globalViewsStateStorageId && e.scope === StorageScope.GLOBAL - && this.globalViewsStatesValue !== this.getStoredGlobalViewsStatesValue() /* This checks if current window changed the value or not */) { - this._globalViewsStatesValue = undefined; - const storedViewsVisibilityStates = PersistentContributableViewsModel.loadGlobalViewsState(this.globalViewsStateStorageId, this.storageService, StorageScope.GLOBAL); - const changedViews: { id: string, visible: boolean }[] = []; - for (const [id, state] of storedViewsVisibilityStates) { - const viewState = this.viewStates.get(id); - if (viewState) { - if (viewState.visibleGlobal !== !state.isHidden) { - changedViews.push({ id, visible: !state.isHidden }); - } - } - } - if (changedViews.length) { - this.doSetVisible(changedViews); - } - } - } - - private saveViewsStates(): void { - this.saveWorkspaceViewsStates(); - this.saveGlobalViewsStates(); - } - - private saveWorkspaceViewsStates(): void { - const storedViewsStates: { [id: string]: IStoredWorkspaceViewState; } = JSON.parse(this.storageService.get(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); - for (const viewDescriptor of this.viewDescriptors) { - const viewState = this.viewStates.get(viewDescriptor.id); - if (viewState) { - storedViewsStates[viewDescriptor.id] = { - collapsed: !!viewState.collapsed, - isHidden: !viewState.visibleWorkspace, - size: viewState.size, - order: viewDescriptor.workspace && viewState ? viewState.order : undefined - }; - } - } - - if (Object.keys(storedViewsStates).length > 0) { - this.storageService.store(this.workspaceViewsStateStorageId, JSON.stringify(storedViewsStates), StorageScope.WORKSPACE); - } else { - this.storageService.remove(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE); - } - } - - private saveGlobalViewsStates(): void { - const storedViewsVisibilityStates = PersistentContributableViewsModel.loadGlobalViewsState(this.globalViewsStateStorageId, this.storageService, StorageScope.GLOBAL); - for (const viewDescriptor of this.viewDescriptors) { - const viewState = this.viewStates.get(viewDescriptor.id); - storedViewsVisibilityStates.set(viewDescriptor.id, { - id: viewDescriptor.id, - isHidden: viewState && viewDescriptor.canToggleVisibility ? !viewState.visibleGlobal : false, - order: !viewDescriptor.workspace && viewState ? viewState.order : undefined - }); - } - this.globalViewsStatesValue = JSON.stringify(values(storedViewsVisibilityStates)); - } - - private _globalViewsStatesValue: string | undefined; - private get globalViewsStatesValue(): string { - if (!this._globalViewsStatesValue) { - this._globalViewsStatesValue = this.getStoredGlobalViewsStatesValue(); - } - - return this._globalViewsStatesValue; - } - - private set globalViewsStatesValue(globalViewsStatesValue: string) { - if (this.globalViewsStatesValue !== globalViewsStatesValue) { - this._globalViewsStatesValue = globalViewsStatesValue; - this.setStoredGlobalViewsStatesValue(globalViewsStatesValue); - } - } - - private getStoredGlobalViewsStatesValue(): string { - return this.storageService.get(this.globalViewsStateStorageId, StorageScope.GLOBAL, '[]'); - } - - private setStoredGlobalViewsStatesValue(value: string): void { - this.storageService.store(this.globalViewsStateStorageId, value, StorageScope.GLOBAL); - } - - private static loadViewsStates(workspaceViewsStateStorageId: string, globalViewsStateStorageId: string, storageService: IStorageService): Map { - const viewStates = new Map(); - const workspaceViewsStates = <{ [id: string]: IStoredWorkspaceViewState; }>JSON.parse(storageService.get(workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); - for (const id of Object.keys(workspaceViewsStates)) { - const workspaceViewState = workspaceViewsStates[id]; - viewStates.set(id, { - visibleGlobal: undefined, - visibleWorkspace: isUndefined(workspaceViewState.isHidden) ? undefined : !workspaceViewState.isHidden, - collapsed: workspaceViewState.collapsed, - order: workspaceViewState.order, - size: workspaceViewState.size - }); - } - - // Migrate to `viewletStateStorageId` - const workspaceVisibilityStates = this.loadGlobalViewsState(globalViewsStateStorageId, storageService, StorageScope.WORKSPACE); - if (workspaceVisibilityStates.size > 0) { - for (const { id, isHidden } of values(workspaceVisibilityStates)) { - let viewState = viewStates.get(id); - // Not migrated to `viewletStateStorageId` - if (viewState) { - if (isUndefined(viewState.visibleWorkspace)) { - viewState.visibleWorkspace = !isHidden; - } - } else { - viewStates.set(id, { - collapsed: undefined, - visibleGlobal: undefined, - visibleWorkspace: !isHidden, - }); - } - } - storageService.remove(globalViewsStateStorageId, StorageScope.WORKSPACE); - } - - const globalViewsStates = this.loadGlobalViewsState(globalViewsStateStorageId, storageService, StorageScope.GLOBAL); - for (const { id, isHidden, order } of values(globalViewsStates)) { - let viewState = viewStates.get(id); - if (viewState) { - viewState.visibleGlobal = !isHidden; - if (!isUndefined(order)) { - viewState.order = order; - } - } else { - viewStates.set(id, { - visibleGlobal: !isHidden, - order, - collapsed: undefined, - visibleWorkspace: undefined, - }); - } - } - return viewStates; - } - - private static loadGlobalViewsState(globalViewsStateStorageId: string, storageService: IStorageService, scope: StorageScope): Map { - const storedValue = >JSON.parse(storageService.get(globalViewsStateStorageId, scope, '[]')); - let hasDuplicates = false; - const storedGlobalViewsState = storedValue.reduce((result, storedState) => { - if (typeof storedState === 'string' /* migration */) { - hasDuplicates = hasDuplicates || result.has(storedState); - result.set(storedState, { id: storedState, isHidden: true }); - } else { - hasDuplicates = hasDuplicates || result.has(storedState.id); - result.set(storedState.id, storedState); - } - return result; - }, new Map()); - - if (hasDuplicates) { - storageService.store(globalViewsStateStorageId, JSON.stringify(values(storedGlobalViewsState)), scope); - } - - return storedGlobalViewsState; - } -} export class ViewsService extends Disposable implements IViewsService { @@ -593,15 +109,15 @@ export class ViewsService extends Disposable implements IViewsService { private onDidRegisterViewContainer(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { this.registerViewletOrPanel(viewContainer, viewContainerLocation); - const viewDescriptorCollection = this.viewDescriptorService.getViewDescriptors(viewContainer); - this.onViewDescriptorsAdded(viewDescriptorCollection.allViewDescriptors, viewContainer); - this._register(viewDescriptorCollection.onDidChangeViews(({ added, removed }) => { + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); + this.onViewDescriptorsAdded(viewContainerModel.allViewDescriptors, viewContainer); + this._register(viewContainerModel.onDidChangeAllViewDescriptors(({ added, removed }) => { this.onViewDescriptorsAdded(added, viewContainer); this.onViewDescriptorsRemoved(removed); })); } - private onViewDescriptorsAdded(views: IViewDescriptor[], container: ViewContainer): void { + private onViewDescriptorsAdded(views: ReadonlyArray, container: ViewContainer): void { const location = this.viewContainersRegistry.getViewContainerLocation(container); if (location === undefined) { return; @@ -665,7 +181,7 @@ export class ViewsService extends Disposable implements IViewsService { } } - private onViewDescriptorsRemoved(views: IViewDescriptor[]): void { + private onViewDescriptorsRemoved(views: ReadonlyArray): void { for (const view of views) { const disposable = this.viewDisposable.get(view); if (disposable) { diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 8588ffe9aac..8335c7352c3 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -6,7 +6,7 @@ import { IAction } from 'vs/base/common/actions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IViewDescriptor, IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewDescriptor, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -16,7 +16,6 @@ import { ViewPaneContainer, ViewPane, IViewPaneOptions } from 'vs/workbench/brow import { Event } from 'vs/base/common/event'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; export interface IViewletViewOptions extends IViewPaneOptions { } @@ -41,14 +40,14 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { - super(viewletId, `${viewletId}.state`, { mergeViewWithContainerWhenSingleView: false }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); + super(viewletId, { mergeViewWithContainerWhenSingleView: false }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); this._register(onDidChangeFilterValue(newFilterValue => { this.filterValue = newFilterValue; this.onFilterChanged(newFilterValue); })); - this._register(this.viewsModel.onDidChangeActiveViews((viewDescriptors) => { - this.updateAllViews(viewDescriptors); + this._register(this.viewContainerModel.onDidChangeActiveViewDescriptors(() => { + this.updateAllViews(this.viewContainerModel.activeViewDescriptors); })); } @@ -63,7 +62,7 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { } this.allViews.get(filterOnValue)!.set(descriptor.id, descriptor); if (this.filterValue && !this.filterValue.includes(filterOnValue)) { - this.viewsModel.setVisible(descriptor.id, false); + this.viewContainerModel.setVisible(descriptor.id, false); } }); } @@ -76,17 +75,17 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { private onFilterChanged(newFilterValue: string[]) { if (this.allViews.size === 0) { - this.updateAllViews(this.viewsModel.viewDescriptors); + this.updateAllViews(this.viewContainerModel.activeViewDescriptors); } - this.getViewsNotForTarget(newFilterValue).forEach(item => this.viewsModel.setVisible(item.id, false)); - this.getViewsForTarget(newFilterValue).forEach(item => this.viewsModel.setVisible(item.id, true)); + this.getViewsNotForTarget(newFilterValue).forEach(item => this.viewContainerModel.setVisible(item.id, false)); + this.getViewsForTarget(newFilterValue).forEach(item => this.viewContainerModel.setVisible(item.id, true)); } getContextMenuActions(): IAction[] { const result: IAction[] = Array.from(this.constantViewDescriptors.values()).map(viewDescriptor => ({ id: `${viewDescriptor.id}.toggleVisibility`, label: viewDescriptor.name, - checked: this.viewsModel.isVisible(viewDescriptor.id), + checked: this.viewContainerModel.isVisible(viewDescriptor.id), enabled: viewDescriptor.canToggleVisibility, run: () => this.toggleViewVisibility(viewDescriptor.id) })); @@ -134,7 +133,7 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { } // Check that allViews is ready if (this.allViews.size === 0) { - this.updateAllViews(this.viewsModel.viewDescriptors); + this.updateAllViews(this.viewContainerModel.activeViewDescriptors); } return panes; } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index b3cd6fdc700..15e8631f1ed 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -38,7 +38,7 @@ import { FileUserDataProvider } from 'vs/workbench/services/userData/common/file import { BACKUPS } from 'vs/platform/environment/common/environment'; import { joinPath } from 'vs/base/common/resources'; import { BrowserStorageService } from 'vs/platform/storage/browser/storageService'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { getThemeTypeSelector, DARK, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; @@ -52,6 +52,23 @@ import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { firstSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; + +interface PanelActivityState { + id: string; + name?: string; + pinned: boolean; + order: number; + visible: boolean; +} + +interface SideBarActivityState { + id: string; + pinned: boolean; + order: number; + visible: boolean; +} + class BrowserMain extends Disposable { @@ -81,6 +98,8 @@ class BrowserMain extends Disposable { // Listeners this.registerListeners(workbench, services.storageService); + this.applyDefaultLayout(services.storageService); + // Driver if (this.configuration.driver) { (async () => this._register(await registerWindowDriver()))(); @@ -101,6 +120,176 @@ class BrowserMain extends Disposable { }); } + private applyDefaultLayout(storageService: BrowserStorageService) { + const { defaultLayout } = this.configuration; + if (!defaultLayout) { + return; + } + + const firstRun = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL); + if (firstRun !== undefined) { + return; + } + + const { sidebar } = defaultLayout; + if (sidebar) { + if (sidebar.visible !== undefined) { + if (sidebar.visible) { + storageService.remove('workbench.sidebar.hidden', StorageScope.WORKSPACE); + } else { + storageService.store('workbench.sidebar.hidden', true, StorageScope.WORKSPACE); + } + } + + if (sidebar.containers !== undefined) { + const sidebarState: SideBarActivityState[] = []; + + let order = -1; + for (const container of sidebar.containers.sort((a, b) => (a.order ?? 1) - (b.order ?? 1))) { + let viewletId; + switch (container.id) { + case 'explorer': + viewletId = 'workbench.view.explorer'; + break; + case 'run': + viewletId = 'workbench.view.debug'; + break; + case 'scm': + viewletId = 'workbench.view.scm'; + break; + case 'search': + viewletId = 'workbench.view.search'; + break; + case 'extensions': + viewletId = 'workbench.view.extensions'; + break; + case 'remote': + viewletId = 'workbench.view.remote'; + break; + default: + viewletId = `workbench.view.extension.${container.id}`; + } + + order = container.order ?? (order + 1); + const state: SideBarActivityState = { + id: viewletId, + order: order, + pinned: true, + visible: true + }; + + if (container.active) { + storageService.store('workbench.sidebar.activeviewletid', viewletId, StorageScope.WORKSPACE); + } else { + if (container.visible !== undefined) { + state.pinned = container.visible; + state.visible = container.visible; + } + } + + sidebarState.push(state); + + if (container.views !== undefined) { + const viewsState: { id: string, isHidden?: boolean, order?: number }[] = []; + const viewsWorkspaceState: { [id: string]: { collapsed: boolean, isHidden?: boolean, size?: number } } = {}; + + for (const view of container.views) { + if (view.order !== undefined || view.visible !== undefined) { + viewsState.push({ + id: view.id, + isHidden: view.visible === undefined ? undefined : !view.visible, + order: view.order === undefined ? undefined : view.order + }); + } + + if (view.collapsed !== undefined) { + viewsWorkspaceState[view.id] = { + collapsed: view.collapsed, + isHidden: view.visible === undefined ? undefined : !view.visible, + }; + } + } + + storageService.store(`${viewletId}.state.hidden`, JSON.stringify(viewsState), StorageScope.GLOBAL); + storageService.store(`${viewletId}.state`, JSON.stringify(viewsWorkspaceState), StorageScope.WORKSPACE); + } + } + + storageService.store('workbench.activity.pinnedViewlets2', JSON.stringify(sidebarState), StorageScope.GLOBAL); + } + } + + const { panel } = defaultLayout; + if (panel) { + if (panel.visible !== undefined) { + if (panel.visible) { + storageService.store('workbench.panel.hidden', false, StorageScope.WORKSPACE); + } else { + storageService.remove('workbench.panel.hidden', StorageScope.WORKSPACE); + } + } + + if (panel.containers !== undefined) { + const panelState: PanelActivityState[] = []; + + let order = -1; + for (const container of panel.containers.sort((a, b) => (a.order ?? 1) - (b.order ?? 1))) { + let name; + let panelId = container.id; + switch (panelId) { + case 'terminal': + name = 'Terminal'; + panelId = 'workbench.panel.terminal'; + break; + case 'debug': + name = 'Debug Console'; + panelId = 'workbench.panel.repl'; + break; + case 'problems': + name = 'Problems'; + panelId = 'workbench.panel.markers'; + break; + case 'output': + name = 'Output'; + panelId = 'workbench.panel.output'; + break; + case 'comments': + name = 'Comments'; + panelId = 'workbench.panel.comments'; + break; + case 'refactor': + name = 'Refactor Preview'; + panelId = 'refactorPreview'; + default: + continue; + } + + order = container.order ?? (order + 1); + const state: PanelActivityState = { + id: panelId, + name: name, + order: order, + pinned: true, + visible: true + }; + + if (container.active) { + storageService.store('workbench.panelpart.activepanelid', panelId, StorageScope.WORKSPACE); + } else { + if (container.visible !== undefined) { + state.pinned = container.visible; + state.visible = container.visible; + } + } + + panelState.push(state); + } + + storageService.store('workbench.panel.pinnedPanels', JSON.stringify(panelState), StorageScope.GLOBAL); + } + } + } + private registerListeners(workbench: Workbench, storageService: BrowserStorageService): void { // Layout diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index c303cd3c1ab..dbdcdccdf92 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -43,6 +43,8 @@ export interface IViewContainerDescriptor { readonly ctorDescriptor: SyncDescriptor; + readonly storageId?: string; + readonly icon?: string | URI; readonly order?: number; @@ -207,11 +209,43 @@ export interface IViewDescriptor { readonly remoteAuthority?: string | string[]; } -export interface IViewDescriptorCollection extends IDisposable { - readonly onDidChangeViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[] }>; - readonly onDidChangeActiveViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[] }>; - readonly activeViewDescriptors: IViewDescriptor[]; - readonly allViewDescriptors: IViewDescriptor[]; +export interface IViewDescriptorRef { + viewDescriptor: IViewDescriptor; + index: number; +} + +export interface IAddedViewDescriptorRef extends IViewDescriptorRef { + collapsed: boolean; + size?: number; +} + +export interface IViewContainerModel { + + readonly title: string; + readonly icon: string | URI | undefined; + readonly onDidChangeContainerInfo: Event<{ title?: boolean, icon?: boolean }>; + + readonly allViewDescriptors: ReadonlyArray; + readonly onDidChangeAllViewDescriptors: Event<{ added: ReadonlyArray, removed: ReadonlyArray }>; + + readonly activeViewDescriptors: ReadonlyArray; + readonly onDidChangeActiveViewDescriptors: Event<{ added: ReadonlyArray, removed: ReadonlyArray }>; + + readonly visibleViewDescriptors: ReadonlyArray; + readonly onDidAddVisibleViewDescriptors: Event; + readonly onDidRemoveVisibleViewDescriptors: Event + readonly onDidMoveVisibleViewDescriptors: Event<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }> + + isVisible(id: string): boolean; + setVisible(id: string, visible: boolean, size?: number): void; + + isCollapsed(id: string): boolean; + setCollapsed(id: string, collapsed: boolean): void; + + getSize(id: string): number | undefined; + setSize(id: string, size: number): void + + move(from: string, to: string): void; } export enum ViewContentPriority { @@ -451,7 +485,7 @@ export interface IViewDescriptorService { moveViewsToContainer(views: IViewDescriptor[], viewContainer: ViewContainer): void; - getViewDescriptors(container: ViewContainer): IViewDescriptorCollection; + getViewContainerModel(container: ViewContainer): IViewContainerModel; getViewDescriptor(viewId: string): IViewDescriptor | null; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts index faf9b538534..289ee656910 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts @@ -352,8 +352,9 @@ const container = Registry.as(ViewContainerExtensions.V hideIfEmpty: true, ctorDescriptor: new SyncDescriptor( ViewPaneContainer, - [BulkEditPane.ID, BulkEditPane.ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }] - ) + [BulkEditPane.ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }] + ), + storageId: BulkEditPane.ID }, ViewContainerLocation.Panel); Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 5255e0e4083..e9c6fb869bb 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -90,7 +90,8 @@ const openPanelKb: IKeybindings = { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: DEBUG_PANEL_ID, name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + storageId: DEBUG_PANEL_ID, focusCommand: { id: OpenDebugConsoleAction.ID, keybindings: openPanelKb diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index e80f6f0670b..ca458d7b809 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -62,7 +62,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { @INotificationService private readonly notificationService: INotificationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { - super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); + super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state))); this._register(this.debugService.onDidNewSession(() => this.updateToolBar())); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 0311e3ce1b1..f1ff8e2206e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -34,7 +34,7 @@ import Severity from 'vs/base/common/severity'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewContainersRegistry, IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewContainersRegistry, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -44,7 +44,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; @@ -359,7 +358,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IPreferencesService private readonly preferencesService: IPreferencesService ) { - super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); + super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); this.searchDelayer = new Delayer(500); this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 52a1b0b6926..9ea99e0896f 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -161,8 +161,6 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor export class ExplorerViewPaneContainer extends ViewPaneContainer { - private static readonly EXPLORER_VIEWS_STATE = 'workbench.explorer.views.state'; - private viewletVisibleContextKey: IContextKey; constructor( @@ -180,7 +178,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { - super(VIEWLET_ID, ExplorerViewPaneContainer.EXPLORER_VIEWS_STATE, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); + super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); this.viewletVisibleContextKey = ExplorerViewletVisibleContext.bindTo(contextKeyService); @@ -263,6 +261,7 @@ export const VIEW_CONTAINER: ViewContainer = Registry.as { return { @@ -560,7 +564,11 @@ export class ReopenResourcesAction extends Action { id: override[1].id, label: override[1].label, description: override[1].active ? 'Currently Active' : undefined, - detail: override[1].detail + detail: override[1].detail, + buttons: resourceExt ? [{ + iconClass: 'codicon-settings-gear', + tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt) + }] : undefined }; }); @@ -568,12 +576,18 @@ export class ReopenResourcesAction extends Action { return; } - items.unshift({ - id: 'default', - label: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"), - description: activeInput instanceof FileEditorInput ? 'Currently Active' : undefined, - detail: builtinProviderDisplayName - }); + if (!items.find(item => item.id === 'default')) { + items.unshift({ + id: 'default', + label: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"), + description: activeInput instanceof FileEditorInput ? 'Currently Active' : undefined, + detail: builtinProviderDisplayName, + buttons: resourceExt ? [{ + iconClass: 'codicon-settings-gear', + tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt) + }] : undefined + }); + } const picker = this.quickInputService.createQuickPick<(IQuickPickItem & { handler?: IOpenEditorOverrideHandler })>(); picker.items = items; @@ -585,6 +599,33 @@ export class ReopenResourcesAction extends Action { picker.dispose(); }); + picker.onDidTriggerItemButton(e => { + const pick = e.item; + const id = pick.id; + resolve(pick); // open the view + picker.dispose(); + + // And persist the setting + if (pick && id) { + const newAssociation: CustomEditorAssociation = { viewType: id, filenamePattern: '*' + resourceExt }; + const currentAssociations = [...this.configurationService.getValue(customEditorsAssociationsSettingId)] || []; + + // First try updating existing association + for (let i = 0; i < currentAssociations.length; ++i) { + const existing = currentAssociations[i]; + if (existing.filenamePattern === newAssociation.filenamePattern) { + currentAssociations.splice(i, 1, newAssociation); + this.configurationService.updateValue(customEditorsAssociationsSettingId, currentAssociations); + return; + } + } + + // Otherwise, create a new one + currentAssociations.unshift(newAssociation); + this.configurationService.updateValue(customEditorsAssociationsSettingId, currentAssociations); + } + }); + picker.show(); }); diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 1f05d487dbe..4072f9303d3 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -115,7 +115,8 @@ const VIEW_CONTAINER: ViewContainer = Registry.as(ViewC name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, hideIfEmpty: true, order: 0, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, Constants.MARKERS_VIEW_STORAGE_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + storageId: Constants.MARKERS_VIEW_STORAGE_ID, focusCommand: { id: ToggleMarkersPanelAction.ID, keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 9bbf4eca7b0..6f598eef643 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -45,13 +45,14 @@ export class FoldingController extends Disposable { // folding icon const cellViewModel = e.target; - const state = viewModel.getFoldingState(cellViewModel); + const modelIndex = viewModel.getCellIndex(cellViewModel); + const state = viewModel.getFoldingState(modelIndex); if (state === CellFoldingState.None) { return; } - viewModel.setFoldingState(cellViewModel, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed); + viewModel.setFoldingState(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed); } return; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts index 8ddc32e89e5..08a53ceadb2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -1174,7 +1174,7 @@ registerAction2(class extends Action2 { return; } - editor.setHiddenAreas([{ start: 1, length: 3 }]); + editor.setHiddenAreas([{ start: 1, end: 3 }]); } }); @@ -1205,7 +1205,7 @@ registerAction2(class extends Action2 { return; } - editor.setHiddenAreas([{ start: 3, length: 2 }]); + editor.setHiddenAreas([{ start: 3, end: 4 }]); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index df2fb08178a..cc453afd092 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -341,6 +341,11 @@ export interface INotebookCellList { updateOptions(options: IListOptions): void; layout(height?: number, width?: number): void; dispose(): void; + + // TODO resolve differences between List and INotebookCellList + getFocus(): number[]; + setFocus(indexes: number[]): void; + setSelection(indexes: number[]): void; } export interface BaseCellRenderTemplate { @@ -442,17 +447,18 @@ export interface CellViewModelStateChangeEvent { } /** - * [start, start + length - 1] + * [start, end] */ export interface ICellRange { /** * zero based index */ start: number; + /** - * One based, includes `start` + * zero based index */ - length: number; + end: number; } @@ -467,21 +473,21 @@ export function reduceCellRanges(_ranges: ICellRange[]): ICellRange[] { let ranges = _ranges.sort((a, b) => a.start - b.start); let result: ICellRange[] = []; let currentRangeStart = ranges[0].start; - let currentRangeEnd = ranges[0].start + ranges[0].length; + let currentRangeEnd = ranges[0].end + 1; for (let i = 0, len = ranges.length; i < len; i++) { let range = ranges[i]; if (range.start > currentRangeEnd) { - result.push({ start: currentRangeStart, length: currentRangeEnd - currentRangeStart }); + result.push({ start: currentRangeStart, end: currentRangeEnd - 1 }); currentRangeStart = range.start; - currentRangeEnd = range.start + range.length; - } else if (range.start + range.length > currentRangeEnd) { - currentRangeEnd = range.start + range.length; + currentRangeEnd = range.end + 1; + } else if (range.end + 1 > currentRangeEnd) { + currentRangeEnd = range.end + 1; } } - result.push({ start: currentRangeStart, length: currentRangeEnd - currentRangeStart }); + result.push({ start: currentRangeStart, end: currentRangeEnd - 1 }); return result; } @@ -499,7 +505,7 @@ export function getVisibleCells(cells: CellViewModel[], hiddenRanges: ICellRange result.push(...cells.slice(start, hiddenRanges[hiddenRangeIndex].start)); } - start = hiddenRanges[hiddenRangeIndex].start + hiddenRanges[hiddenRangeIndex].length; + start = hiddenRanges[hiddenRangeIndex].end + 1; hiddenRangeIndex++; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index dcc4626d35f..258ff4ec28d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -280,7 +280,11 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return this.webview?.webview; } - onHide() { + onWillHide() { + if (this.input && this.input instanceof NotebookEditorInput && !this.input.isDisposed()) { + this.saveTextEditorViewState(this.input); + } + this.editorFocus?.set(false); if (this.webview) { this.localStore.clear(); @@ -312,6 +316,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { focus() { super.focus(); this.editorFocus?.set(true); + this.list?.domFocus(); } async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { @@ -353,10 +358,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } clearInput(): void { - if (this.input && this.input instanceof NotebookEditorInput && !this.input.isDisposed()) { - this.saveTextEditorViewState(this.input); - } - super.clearInput(); } @@ -377,6 +378,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); } + await this.webview.waitForInitialization(); + this.eventDispatcher = new NotebookEventDispatcher(); this.notebookViewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model, this.eventDispatcher, this.getLayoutInfo()); this.editorEditable?.set(!!this.notebookViewModel.metadata?.editable); @@ -429,6 +432,10 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.list!.layout(); + this.restoreTextEditorViewState(viewState); + } + + private restoreTextEditorViewState(viewState: INotebookEditorViewState | undefined): void { if (viewState?.scrollPosition !== undefined) { this.list!.scrollTop = viewState!.scrollPosition.top; this.list!.scrollLeft = viewState!.scrollPosition.left; @@ -436,6 +443,18 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.list!.scrollTop = 0; this.list!.scrollLeft = 0; } + + const focusIdx = typeof viewState?.focus === 'number' ? viewState.focus : 0; + this.list!.setFocus([focusIdx]); + this.list!.setSelection([focusIdx]); + + if (viewState?.editorFocused) { + this.list?.focusView(); + const cell = this.notebookViewModel?.viewCells[focusIdx]; + if (cell) { + cell.focusMode = CellFocusMode.Editor; + } + } } private saveTextEditorViewState(input: NotebookEditorInput): void { @@ -444,7 +463,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { if (this.list) { state.scrollPosition = { left: this.list.scrollLeft, top: this.list.scrollTop }; let cellHeights: { [key: number]: number } = {}; - for (let i = 0; i < this.viewModel!.viewCells.length; i++) { + for (let i = 0; i < this.viewModel!.length; i++) { const elm = this.viewModel!.viewCells[i] as CellViewModel; if (elm.cellKind === CellKind.Code) { cellHeights[i] = elm.layoutInfo.totalHeight; @@ -454,6 +473,19 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } state.cellTotalHeights = cellHeights; + + const focus = this.list.getFocus()[0]; + if (focus) { + const element = this.notebookViewModel!.viewCells[focus]; + const itemDOM = this.list?.domElementOfElement(element!); + let editorFocused = false; + if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { + editorFocused = true; + } + + state.editorFocused = editorFocused; + state.focus = focus; + } } this.editorMemento.saveEditorState(this.group, input.resource, state); @@ -616,7 +648,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { async moveCellDown(cell: ICellViewModel): Promise { const index = this.notebookViewModel!.getCellIndex(cell); - if (index === this.notebookViewModel!.viewCells.length - 1) { + if (index === this.notebookViewModel!.length - 1) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 5b7bd3ab364..9a47fcbb1e9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IListRenderer, IListVirtualDelegate, ListError } from 'vs/base/browser/ui/list/list'; -import { IListStyles, IStyleController, MouseController } from 'vs/base/browser/ui/list/listWidget'; +import { IListStyles, IStyleController, MouseController, IListOptions } from 'vs/base/browser/ui/list/listWidget'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; @@ -120,7 +120,7 @@ export class NotebookCellList extends WorkbenchList implements ID } - protected createMouseController(): MouseController { + protected createMouseController(_options: IListOptions): MouseController { return new NotebookMouseController(this); } @@ -189,7 +189,7 @@ export class NotebookCellList extends WorkbenchList implements ID if (newRanges.length === oldRanges.length) { let hasDifference = false; for (let i = 0; i < newRanges.length; i++) { - if (!(newRanges[i].start === oldRanges[i].start && newRanges[i].length === oldRanges[i].length)) { + if (!(newRanges[i].start === oldRanges[i].start && newRanges[i].end === oldRanges[i].end)) { hasDifference = true; break; } @@ -217,12 +217,12 @@ export class NotebookCellList extends WorkbenchList implements ID ret.push(1); } - ret.push(newRanges[index].length + 1); - start = newRanges[index].start + newRanges[index].length; + ret.push(newRanges[index].end - newRanges[index].start + 1 + 1); + start = newRanges[index].end + 1; index++; } - for (let i = start; i < this._viewModel!.viewCells.length; i++) { + for (let i = start; i < this._viewModel!.length; i++) { ret.push(1); } @@ -237,7 +237,7 @@ export class NotebookCellList extends WorkbenchList implements ID // console.log(this.hiddenRangesPrefixSum.getAccumulatedValue(i)); // } - // for (let i = 0; i < this._viewModel!.viewCells.length; i++) { + // for (let i = 0; i < this._viewModel!.length; i++) { // console.log(this.hiddenRangesPrefixSum.getIndexOf(i)); // } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index f3c8121062e..e28677dd2ac 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -18,6 +18,7 @@ import { CELL_MARGIN, CELL_RUN_GUTTER } from 'vs/workbench/contrib/notebook/brow import { Emitter, Event } from 'vs/base/common/event'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { isWeb } from 'vs/base/common/platform'; export interface IDimentionMessage { __vscode_notebook_message: boolean; @@ -84,7 +85,7 @@ type IMessage = IDimentionMessage | IScrollAckMessage | IWheelMessage; let version = 0; export class BackLayerWebView extends Disposable { element: HTMLElement; - webview: WebviewElement; + webview!: WebviewElement; insetMapping: Map = new Map(); reversedInsetMapping: Map = new Map(); preloadsCache: Map = new Map(); @@ -92,12 +93,13 @@ export class BackLayerWebView extends Disposable { rendererRootsCache: URI[] = []; private readonly _onMessage = this._register(new Emitter()); public readonly onMessage: Event = this._onMessage.event; + private _initalized: Promise; constructor( public notebookEditor: INotebookEditor, - @IWebviewService webviewService: IWebviewService, - @IOpenerService openerService: IOpenerService, + @IWebviewService readonly webviewService: IWebviewService, + @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, ) { super(); @@ -111,8 +113,40 @@ export class BackLayerWebView extends Disposable { const pathsPath = getPathFromAmdModule(require, 'vs/loader.js'); const loader = URI.file(pathsPath).with({ scheme: WebviewResourceScheme }); - const outputNodePadding = 8; - let content = /* html */` + let coreDependencies = ''; + let resolveFunc: () => void; + + this._initalized = new Promise((resolve, reject) => { + resolveFunc = resolve; + }); + + if (!isWeb) { + coreDependencies = ``; + const htmlContent = this.generateContent(8, coreDependencies); + this.initialize(htmlContent); + resolveFunc!(); + } else { + fetch(pathsPath).then(async response => { + if (response.status !== 200) { + throw new Error(response.statusText); + } + + const loaderJs = await response.text(); + + coreDependencies = ` + +`; + const htmlContent = this.generateContent(8, coreDependencies); + this.initialize(htmlContent); + resolveFunc!(); + }); + } + } + + generateContent(outputNodePadding: number, coreDependencies: string) { + return /* html */` @@ -134,7 +168,7 @@ export class BackLayerWebView extends Disposable { - + ${coreDependencies}
`; + } - this.webview = this._createInset(webviewService, content); + initialize(content: string) { + this.webview = this._createInset(this.webviewService, content); this.webview.mountTo(this.element); this._register(this.webview.onDidClickLink(link => { - openerService.open(link, { fromUserGesture: true }); + this.openerService.open(link, { fromUserGesture: true }); })); this._register(this.webview.onMessage((data: IMessage) => { @@ -356,6 +392,10 @@ export class BackLayerWebView extends Disposable { })); } + async waitForInitialization() { + await this._initalized; + } + private _createInset(webviewService: IWebviewService, content: string) { const rootPath = URI.file(path.dirname(getPathFromAmdModule(require, ''))); this.localResourceRootsCache = [...this.notebookService.getNotebookProviderResourceRoots(), rootPath]; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts index ec0f9aaef87..92768b04249 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts @@ -144,7 +144,7 @@ export class FoldingModel extends Disposable { const cellRanges: ICellRange[] = []; for (let i = 0; i < newRegions.length; i++) { const region = newRegions.toRegion(i); - cellRanges.push({ start: region.startLineNumber - 1, length: region.endLineNumber - region.startLineNumber + 1 }); + cellRanges.push({ start: region.startLineNumber - 1, end: region.endLineNumber - 1 }); } // remove old tracked ranges and add new ones @@ -165,6 +165,7 @@ export enum CellFoldingState { export interface FoldingRegionDelegate { onDidFoldingRegionChanged: Event; - getFoldingStartIndex(cell: CellViewModel): number; - getFoldingState(cell: CellViewModel): CellFoldingState; + getCellIndex(cell: CellViewModel): number; + getFoldingStartIndex(index: number): number; + getFoldingState(index: number): CellFoldingState; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index aeefc169baa..1d12a265de5 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -39,7 +39,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie readonly onDidChangeLayout = this._onDidChangeLayout.event; get foldingState() { - return this.foldingDelegate.getFoldingState(this); + return this.foldingDelegate.getFoldingState(this.foldingDelegate.getCellIndex(this)); } constructor( diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index e3098e1f9c9..c0989ec3922 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -33,6 +33,8 @@ export interface INotebookEditorViewState { editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState | null }; cellTotalHeights?: { [key: number]: number }; scrollPosition?: { left: number; top: number; }; + focus?: number; + editorFocused?: boolean; } export interface ICellModelDecorations { @@ -128,6 +130,7 @@ let MODEL_ID = 0; export class NotebookViewModel extends Disposable implements FoldingRegionDelegate { private _localStore: DisposableStore = this._register(new DisposableStore()); private _viewCells: CellViewModel[] = []; + private _handleToViewCellMapping = new Map(); private _currentTokenSource: CancellationTokenSource | undefined; @@ -143,6 +146,14 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega return this._viewCells; } + set viewCells(_: ICellViewModel[]) { + throw new Error('NotebookViewModel.viewCells is readonly'); + } + + get length(): number { + return this._viewCells.length; + } + get notebookDocument() { return this._model.notebook; } @@ -244,6 +255,10 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega return createCellViewModel(this.instantiationService, this, cell); }); + this._viewCells.forEach(cell => { + this._handleToViewCellMapping.set(cell.handle, cell); + }); + this._foldingModel = new FoldingModel(); this._foldingModel.attachViewModel(this); @@ -251,43 +266,30 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega this._updateFoldingRanges(); })); } - getFoldingStartIndex(cell: CellViewModel): number { - const modelIndex = this.viewCells.indexOf(cell); - if (modelIndex < 0) { - return -1; - } - const range = this._foldingModel.regions.findRange(modelIndex + 1); + getFoldingStartIndex(index: number): number { + const range = this._foldingModel.regions.findRange(index + 1); const startIndex = this._foldingModel.regions.getStartLineNumber(range) - 1; return startIndex; } - getFoldingState(cell: CellViewModel): CellFoldingState { - const modelIndex = this.viewCells.indexOf(cell); - if (modelIndex < 0) { - return -1; - } - - const range = this._foldingModel.regions.findRange(modelIndex + 1); + getFoldingState(index: number): CellFoldingState { + const range = this._foldingModel.regions.findRange(index + 1); const startIndex = this._foldingModel.regions.getStartLineNumber(range) - 1; - if (startIndex !== modelIndex) { + if (startIndex !== index) { return CellFoldingState.None; } return this._foldingModel.regions.isCollapsed(range) ? CellFoldingState.Collapsed : CellFoldingState.Expanded; } - setFoldingState(cell: CellViewModel, state: CellFoldingState): void { - const modelIndex = this.viewCells.indexOf(cell); - if (modelIndex < 0) { - return; - } + setFoldingState(index: number, state: CellFoldingState): void { - const range = this._foldingModel.regions.findRange(modelIndex + 1); + const range = this._foldingModel.regions.findRange(index + 1); const startIndex = this._foldingModel.regions.getStartLineNumber(range) - 1; - if (startIndex !== modelIndex) { + if (startIndex !== index) { return; } @@ -318,13 +320,13 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega continue; } - if (!updateHiddenAreas && k < this._hiddenRanges.length && this._hiddenRanges[k].start + 1 === startLineNumber && (this._hiddenRanges[k].start + this._hiddenRanges[k].length) === endLineNumber) { + if (!updateHiddenAreas && k < this._hiddenRanges.length && this._hiddenRanges[k].start + 1 === startLineNumber && (this._hiddenRanges[k].end + 1) === endLineNumber) { // reuse the old ranges newHiddenAreas.push(this._hiddenRanges[k]); k++; } else { updateHiddenAreas = true; - newHiddenAreas.push({ start: startLineNumber - 1, length: endLineNumber - startLineNumber + 1 }); + newHiddenAreas.push({ start: startLineNumber - 1, end: endLineNumber - 1 }); } lastCollapsedStart = startLineNumber; lastCollapsedEnd = endLineNumber; @@ -361,6 +363,10 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega return this._viewCells.indexOf(cell as CellViewModel); } + hasCell(cell: ICellViewModel) { + return this._handleToViewCellMapping.has(cell.handle); + } + getVersionId() { return this._model.notebook.versionId; } @@ -379,10 +385,10 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega this._decorationsTree.resolveNode(node, versionId); } if (node.range === null) { - return { start: node.cachedAbsoluteStart - 1, length: node.cachedAbsoluteEnd - node.cachedAbsoluteStart + 1 }; + return { start: node.cachedAbsoluteStart - 1, end: node.cachedAbsoluteEnd - 1 }; } - return { start: node.range.startLineNumber - 1, length: node.range.endLineNumber - node.range.startLineNumber + 1 }; + return { start: node.range.startLineNumber - 1, end: node.range.endLineNumber - 1 }; } setTrackedRange(id: string | null, newRange: ICellRange | null, newStickiness: TrackedRangeStickiness): string | null { @@ -393,7 +399,7 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega return null; } - return this._deltaCellDecorationsImpl(0, [], [{ range: new Range(newRange.start + 1, 1, newRange.start + newRange.length, 1), options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0]; + return this._deltaCellDecorationsImpl(0, [], [{ range: new Range(newRange.start + 1, 1, newRange.end + 1, 1), options: TRACKED_RANGE_OPTIONS[newStickiness] }])[0]; } if (!newRange) { @@ -404,7 +410,7 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega } this._decorationsTree.delete(node); - node.reset(this.getVersionId(), newRange.start, newRange.start + newRange.length, new Range(newRange.start + 1, 1, newRange.start + newRange.length, 1)); + node.reset(this.getVersionId(), newRange.start, newRange.end + 1, new Range(newRange.start + 1, 1, newRange.end + 1, 1)); node.setOptions(TRACKED_RANGE_OPTIONS[newStickiness]); this._decorationsTree.insert(node); return node.id; @@ -476,13 +482,17 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega private _insertCellDelegate(insertIndex: number, insertCell: CellViewModel) { this._viewCells!.splice(insertIndex, 0, insertCell); + this._handleToViewCellMapping.set(insertCell.handle, insertCell); this._model.insertCell(insertCell.model, insertIndex); this._localStore.add(insertCell); this._onDidChangeViewCells.fire({ synchronous: true, splices: [[insertIndex, 0, [insertCell]]] }); } private _deleteCellDelegate(deleteIndex: number, cell: ICell) { + const deleteCell = this._viewCells[deleteIndex]; this._viewCells.splice(deleteIndex, 1); + this._handleToViewCellMapping.delete(deleteCell.handle); + this._model.deleteCell(deleteIndex); this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); } @@ -491,6 +501,7 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega const cell = this._model.notebook.createCellTextModel(source, language, type, [], undefined); let newCell: CellViewModel = createCellViewModel(this.instantiationService, this, cell); this._viewCells!.splice(index, 0, newCell); + this._handleToViewCellMapping.set(newCell.handle, newCell); this._model.insertCell(cell, index); this._localStore.add(newCell); this.undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, { @@ -506,6 +517,8 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega insertCell(index: number, cell: NotebookCellTextModel, synchronous: boolean): CellViewModel { let newCell: CellViewModel = createCellViewModel(this.instantiationService, this, cell); this._viewCells!.splice(index, 0, newCell); + this._handleToViewCellMapping.set(newCell.handle, newCell); + this._model.insertCell(newCell.model, index); this._localStore.add(newCell); this.undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, { @@ -521,6 +534,8 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega deleteCell(index: number, synchronous: boolean) { let viewCell = this._viewCells[index]; this._viewCells.splice(index, 1); + this._handleToViewCellMapping.delete(viewCell.handle); + this._model.deleteCell(index); this.undoService.pushElement(new DeleteCellEdit(this.uri, index, viewCell, { @@ -565,8 +580,8 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega } saveEditorViewState(): INotebookEditorViewState { - const state: { [key: number]: boolean } = {}; - this._viewCells.filter(cell => cell.editState === CellEditState.Editing).forEach(cell => state[cell.model.handle] = true); + const editingCells: { [key: number]: boolean } = {}; + this._viewCells.filter(cell => cell.editState === CellEditState.Editing).forEach(cell => editingCells[cell.model.handle] = true); const editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState } = {}; this._viewCells.map(cell => ({ handle: cell.model.handle, state: cell.saveEditorViewState() })).forEach(viewState => { if (viewState.state) { @@ -575,8 +590,8 @@ export class NotebookViewModel extends Disposable implements FoldingRegionDelega }); return { - editingCells: state, - editorViewStates: editorViewStates + editingCells, + editorViewStates }; } diff --git a/src/vs/workbench/contrib/notebook/test/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/test/notebookFolding.test.ts index 7f919d1df45..667b1c8f6e1 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookFolding.test.ts @@ -10,7 +10,6 @@ import { withTestNotebook, TestCell } from 'vs/workbench/contrib/notebook/test/t import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { FoldingModel, CellFoldingState } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; -import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; suite('Notebook Folding', () => { const instantiationService = new TestInstantiationService(); @@ -96,10 +95,9 @@ suite('Notebook Folding', () => { [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { - const firstCell = viewModel.viewCells[0] as CellViewModel; - viewModel.setFoldingState(firstCell, CellFoldingState.Collapsed); + viewModel.setFoldingState(0, CellFoldingState.Collapsed); assert.deepEqual(viewModel.getHiddenRanges(), [ - { start: 1, length: 6 } + { start: 1, end: 6 } ]); } ); @@ -118,10 +116,9 @@ suite('Notebook Folding', () => { [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { - const thirdCell = viewModel.viewCells[2] as CellViewModel; - viewModel.setFoldingState(thirdCell, CellFoldingState.Collapsed); + viewModel.setFoldingState(2, CellFoldingState.Collapsed); assert.deepEqual(viewModel.getHiddenRanges(), [ - { start: 3, length: 2 } + { start: 3, end: 4 } ]); } ); @@ -140,10 +137,9 @@ suite('Notebook Folding', () => { [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { - const thirdCell = viewModel.viewCells[2] as CellViewModel; - viewModel.setFoldingState(thirdCell, CellFoldingState.Collapsed); + viewModel.setFoldingState(2, CellFoldingState.Collapsed); assert.deepEqual(viewModel.getHiddenRanges(), [ - { start: 3, length: 4 } + { start: 3, end: 6 } ]); } ); @@ -164,35 +160,35 @@ suite('Notebook Folding', () => { [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { - viewModel.setFoldingState(viewModel.viewCells[0] as CellViewModel, CellFoldingState.Collapsed); + viewModel.setFoldingState(0, CellFoldingState.Collapsed); assert.deepEqual(viewModel.getHiddenRanges(), [ - { start: 1, length: 1 } + { start: 1, end: 1 } ]); - viewModel.setFoldingState(viewModel.viewCells[5] as CellViewModel, CellFoldingState.Collapsed); - viewModel.setFoldingState(viewModel.viewCells[2] as CellViewModel, CellFoldingState.Collapsed); + viewModel.setFoldingState(5, CellFoldingState.Collapsed); + viewModel.setFoldingState(2, CellFoldingState.Collapsed); assert.deepEqual(viewModel.getHiddenRanges(), [ - { start: 1, length: 1 }, - { start: 3, length: 4 } + { start: 1, end: 1 }, + { start: 3, end: 6 } ]); - viewModel.setFoldingState(viewModel.viewCells[2] as CellViewModel, CellFoldingState.Expanded); + viewModel.setFoldingState(2, CellFoldingState.Expanded); assert.deepEqual(viewModel.getHiddenRanges(), [ - { start: 1, length: 1 }, - { start: 6, length: 1 } + { start: 1, end: 1 }, + { start: 6, end: 6 } ]); viewModel.insertCell(7, new TestCell(viewModel.viewType, 7, ['var c = 8;'], 'markdown', CellKind.Code, []), true); assert.deepEqual(viewModel.getHiddenRanges(), [ - { start: 1, length: 1 }, - { start: 6, length: 2 } + { start: 1, end: 1 }, + { start: 6, end: 7 } ]); viewModel.insertCell(1, new TestCell(viewModel.viewType, 8, ['var c = 9;'], 'markdown', CellKind.Code, []), true); assert.deepEqual(viewModel.getHiddenRanges(), [ // the first collapsed range is now expanded as we insert content into it. - // { start: 1, length: 2 }, - { start: 7, length: 2 } + // { start: 1,}, + { start: 7, end: 8 } ]); } ); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 3285bcb7d3d..ccce4ad0f52 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -251,7 +251,7 @@ function getVisibleCells(cells: any[], hiddenRanges: ICellRange[]) { result.push(...cells.slice(start, hiddenRanges[hiddenRangeIndex].start)); } - start = hiddenRanges[hiddenRangeIndex].start + hiddenRanges[hiddenRangeIndex].length; + start = hiddenRanges[hiddenRangeIndex].end + 1; hiddenRangeIndex++; } @@ -281,40 +281,46 @@ suite('NotebookViewModel Decorations', () => { [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { - const trackedId = viewModel.setTrackedRange('test', { start: 1, length: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); + const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, - length: 2 + + end: 2, }); viewModel.insertCell(0, new TestCell(viewModel.viewType, 5, ['var d = 6;'], 'javascript', CellKind.Code, []), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 2, - length: 2 + + end: 3 }); viewModel.deleteCell(0, true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, - length: 2 + + end: 2 }); viewModel.insertCell(3, new TestCell(viewModel.viewType, 6, ['var d = 7;'], 'javascript', CellKind.Code, []), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, - length: 3 + + end: 3 }); viewModel.deleteCell(3, true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, - length: 2 + + end: 2 }); viewModel.deleteCell(1, true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 0, - length: 2 + + end: 1 }); } ); @@ -335,22 +341,25 @@ suite('NotebookViewModel Decorations', () => { [['var e = 7;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { - const trackedId = viewModel.setTrackedRange('test', { start: 1, length: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); + const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, - length: 3 + + end: 3 }); viewModel.insertCell(5, new TestCell(viewModel.viewType, 8, ['var d = 9;'], 'javascript', CellKind.Code, []), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, - length: 3 + + end: 3 }); viewModel.insertCell(4, new TestCell(viewModel.viewType, 9, ['var d = 10;'], 'javascript', CellKind.Code, []), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, - length: 4 + + end: 4 }); } ); @@ -358,20 +367,20 @@ suite('NotebookViewModel Decorations', () => { test('reduce range', function () { assert.deepEqual(reduceCellRanges([ - { start: 0, length: 2 }, - { start: 1, length: 2 }, - { start: 4, length: 2 } + { start: 0, end: 1 }, + { start: 1, end: 2 }, + { start: 4, end: 6 } ]), [ - { start: 0, length: 3 }, - { start: 4, length: 2 } + { start: 0, end: 2 }, + { start: 4, end: 6 } ]); assert.deepEqual(reduceCellRanges([ - { start: 0, length: 2 }, - { start: 1, length: 2 }, - { start: 3, length: 2 } + { start: 0, end: 1 }, + { start: 1, end: 2 }, + { start: 3, end: 4 } ]), [ - { start: 0, length: 5 } + { start: 0, end: 4 } ]); }); @@ -381,7 +390,7 @@ suite('NotebookViewModel Decorations', () => { assert.deepEqual( getVisibleCells( [1, 2, 3, 4, 5], - [{ start: 1, length: 2 }] + [{ start: 1, end: 2 }] ), [1, 4, 5] ); @@ -390,8 +399,8 @@ suite('NotebookViewModel Decorations', () => { getVisibleCells( [1, 2, 3, 4, 5, 6, 7, 8, 9], [ - { start: 1, length: 2 }, - { start: 4, length: 2 } + { start: 1, end: 2 }, + { start: 4, end: 5 } ] ), [1, 4, 7, 8, 9] @@ -400,15 +409,15 @@ suite('NotebookViewModel Decorations', () => { const original = getVisibleCells( [1, 2, 3, 4, 5, 6, 7, 8, 9], [ - { start: 1, length: 2 }, - { start: 4, length: 2 } + { start: 1, end: 2 }, + { start: 4, end: 5 } ] ); const modified = getVisibleCells( [1, 2, 3, 4, 5, 6, 7, 8, 9], [ - { start: 2, length: 3 } + { start: 2, end: 4 } ] ); diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 5f954a2b6a1..b128d30b4da 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -63,7 +63,8 @@ const VIEW_CONTAINER: ViewContainer = Registry.as(ViewC id: OUTPUT_VIEW_ID, name: nls.localize('output', "Output"), order: 1, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [OUTPUT_VIEW_ID, OUTPUT_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [OUTPUT_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + storageId: OUTPUT_VIEW_ID, hideIfEmpty: true, focusCommand: { id: toggleOutputAcitonId, keybindings: toggleOutputActionKeybindings } }, ViewContainerLocation.Panel); diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index e764dc28dc9..66ce39f9cec 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -96,9 +96,9 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider = []; const getViewEntriesForViewlet = (viewlet: ViewletDescriptor, viewContainer: ViewContainer): IViewQuickPickItem[] => { - const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); const result: IViewQuickPickItem[] = []; - for (const view of viewDescriptors.allViewDescriptors) { + for (const view of viewContainerModel.allViewDescriptors) { if (this.contextKeyService.contextMatchesRules(view.when)) { result.push({ label: view.name, @@ -176,7 +176,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider(ViewExtensions.ViewContainersRegistry).get(container.id); if (viewContainer?.hideIfEmpty) { - return this.viewDescriptorService.getViewDescriptors(viewContainer).activeViewDescriptors.length > 0; + return this.viewDescriptorService.getViewContainerModel(viewContainer).activeViewDescriptors.length > 0; } return true; diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index d595fd7dccf..5d4bb056f39 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -19,7 +19,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { FilterViewPaneContainer } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { VIEWLET_ID } from 'vs/workbench/contrib/remote/common/remote.contribution'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IViewDescriptor, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry, IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewDescriptor, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -46,7 +46,6 @@ import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remo import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { startsWith } from 'vs/base/common/strings'; import { TunnelPanelDescriptor, TunnelViewModel, forwardedPortsViewEnabled } from 'vs/workbench/contrib/remote/browser/tunnelView'; -import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 12c19d63ba0..2a490cadde6 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -52,6 +52,7 @@ Registry.as(ViewContainerExtensions.ViewContainersRegis id: VIEWLET_ID, name: localize('source control', "Source Control"), ctorDescriptor: new SyncDescriptor(SCMViewPaneContainer), + storageId: 'workbench.scm.views.state', icon: Codicon.sourceControl.classNames, order: 2 }, ViewContainerLocation.Sidebar); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 75544984190..30b7511a215 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -24,12 +24,11 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IViewsRegistry, Extensions, IViewDescriptorService, IViewDescriptor } from 'vs/workbench/common/views'; +import { IViewsRegistry, Extensions, IViewDescriptorService, IViewDescriptor, IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { RepositoryPane, RepositoryViewDescriptor } from 'vs/workbench/contrib/scm/browser/repositoryPane'; import { MainPaneDescriptor, MainPane, IViewModel } from 'vs/workbench/contrib/scm/browser/mainPane'; import { ViewPaneContainer, IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import type { IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { debounce } from 'vs/base/common/decorators'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -81,8 +80,6 @@ export class EmptyPaneDescriptor implements IViewDescriptor { export class SCMViewPaneContainer extends ViewPaneContainer implements IViewModel { - private static readonly STATE_KEY = 'workbench.scm.views.state'; - private menus: SCMMenus; private _repositories: ISCMRepository[] = []; @@ -105,7 +102,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode } get onDidChangeVisibleRepositories(): Event { - const modificationEvent = Event.debounce(Event.any(this.viewsModel.onDidAdd, this.viewsModel.onDidRemove), () => null, 0); + const modificationEvent = Event.debounce(Event.any(this.viewContainerModel.onDidAddVisibleViewDescriptors, this.viewContainerModel.onDidRemoveVisibleViewDescriptors), () => null, 0); return Event.map(modificationEvent, () => this.visibleRepositories); } @@ -127,7 +124,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService ) { - super(VIEWLET_ID, SCMViewPaneContainer.STATE_KEY, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); + super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); this.menus = instantiationService.createInstance(SCMMenus, undefined); this._register(this.menus.onDidChangeTitle(this.updateTitleArea, this)); @@ -144,14 +141,14 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('scm.alwaysShowProviders') && configurationService.getValue('scm.alwaysShowProviders')) { - this.viewsModel.setVisible(MainPane.ID, true); + this.viewContainerModel.setVisible(MainPane.ID, true); } })); this.repositoryCountKey = contextKeyService.createKey('scm.providerCount', 0); - this._register(this.viewsModel.onDidAdd(this.onDidShowView, this)); - this._register(this.viewsModel.onDidRemove(this.onDidHideView, this)); + this._register(this.viewContainerModel.onDidAddVisibleViewDescriptors(this.onDidShowView, this)); + this._register(this.viewContainerModel.onDidRemoveVisibleViewDescriptors(this.onDidHideView, this)); } create(parent: HTMLElement): void { @@ -218,8 +215,8 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode @debounce(0) private afterOnDidHideView(): void { - if (this.repositoryCountKey.get()! > 0 && this.viewDescriptors.every(d => !this.viewsModel.isVisible(d.id))) { - this.viewsModel.setVisible(this.viewDescriptors[0].id, true); + if (this.repositoryCountKey.get()! > 0 && this.viewDescriptors.every(d => !this.viewContainerModel.isVisible(d.id))) { + this.viewContainerModel.setVisible(this.viewDescriptors[0].id, true); } } @@ -286,9 +283,9 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode } setVisibleRepositories(repositories: ISCMRepository[]): void { - const visibleViewDescriptors = this.viewsModel.visibleViewDescriptors; + const visibleViewDescriptors = this.viewContainerModel.visibleViewDescriptors; - const toSetVisible = this.viewsModel.viewDescriptors + const toSetVisible = this.viewContainerModel.activeViewDescriptors .filter((d): d is RepositoryViewDescriptor => d instanceof RepositoryViewDescriptor && repositories.indexOf(d.repository) > -1 && visibleViewDescriptors.indexOf(d) === -1); const toSetInvisible = visibleViewDescriptors @@ -306,11 +303,11 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode } } - this.viewsModel.setVisible(viewDescriptor.id, false); + this.viewContainerModel.setVisible(viewDescriptor.id, false); } for (const viewDescriptor of toSetVisible) { - this.viewsModel.setVisible(viewDescriptor.id, true, size); + this.viewContainerModel.setVisible(viewDescriptor.id, true, size); } } } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index ea022fe841c..0a1e9d7832f 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -505,7 +505,7 @@ class ShowAllSymbolsAction extends Action { const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, name: nls.localize('name', "Search"), - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), hideIfEmpty: true, icon: searchViewIcon.classNames, order: 1 diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 0b5b7d0fcaf..e842ee3aa16 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -68,7 +68,8 @@ if (platform.isWeb) { const VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: TERMINAL_VIEW_ID, name: nls.localize('terminal', "Terminal"), - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [TERMINAL_VIEW_ID, TERMINAL_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [TERMINAL_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + storageId: TERMINAL_VIEW_ID, focusCommand: { id: TERMINAL_COMMAND_ID.FOCUS }, hideIfEmpty: true, order: 3 diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 667b3ed88da..e5cd0501514 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -704,7 +704,7 @@ export class TerminalService implements ITerminalService { const location = this._viewDescriptorService.getViewLocation(TERMINAL_VIEW_ID); if (location === ViewContainerLocation.Panel) { const panel = this._viewDescriptorService.getViewContainer(TERMINAL_VIEW_ID); - if (panel && this._viewDescriptorService.getViewDescriptors(panel).activeViewDescriptors.length === 1) { + if (panel && this._viewDescriptorService.getViewContainerModel(panel).activeViewDescriptors.length === 1) { this._layoutService.setPanelHidden(true); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts index 6e819791e4b..89ba4e168f5 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -40,7 +40,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { name: localize('sync preferences', "Preferences Sync"), ctorDescriptor: new SyncDescriptor( ViewPaneContainer, - ['workbench.view.sync', `workbench.view.sync.state`, { mergeViewWithContainerWhenSingleView: true }] + ['workbench.view.sync', { mergeViewWithContainerWhenSingleView: true }] ), icon: Codicon.sync.classNames, hideIfEmpty: true, diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 64a256b3fca..40df3900401 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -100,7 +100,7 @@ class WebviewSession extends Disposable { return; } } - callback({ cancel: false, responseHeaders: details.responseHeaders }); + callback({ cancel: false }); }); })); } diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 7c52f852fdf..13f75686a26 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ViewContainerLocation, IViewDescriptorService, ViewContainer, IViewsRegistry, IViewContainersRegistry, IViewDescriptor, Extensions as ViewExtensions, IViewDescriptorCollection } from 'vs/workbench/common/views'; -import { IContextKey, RawContextKey, IContextKeyService, IReadableSet, IContextKeyChangeEvent } from 'vs/platform/contextkey/common/contextkey'; +import { ViewContainerLocation, IViewDescriptorService, ViewContainer, IViewsRegistry, IViewContainersRegistry, IViewDescriptor, Extensions as ViewExtensions } from 'vs/workbench/common/views'; +import { IContextKey, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -13,163 +13,11 @@ import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneCont import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; -import { firstIndex } from 'vs/base/common/arrays'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { generateUuid } from 'vs/base/common/uuid'; - -class CounterSet implements IReadableSet { - - private map = new Map(); - - add(value: T): CounterSet { - this.map.set(value, (this.map.get(value) || 0) + 1); - return this; - } - - delete(value: T): boolean { - let counter = this.map.get(value) || 0; - - if (counter === 0) { - return false; - } - - counter--; - - if (counter === 0) { - this.map.delete(value); - } else { - this.map.set(value, counter); - } - - return true; - } - - has(value: T): boolean { - return this.map.has(value); - } -} - -interface IViewItem { - viewDescriptor: IViewDescriptor; - active: boolean; -} - -class ViewDescriptorCollection extends Disposable implements IViewDescriptorCollection { - - private contextKeys = new CounterSet(); - private items: IViewItem[] = []; - - private _onDidChangeViews: Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._register(new Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }>()); - readonly onDidChangeViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._onDidChangeViews.event; - - private _onDidChangeActiveViews: Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._register(new Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }>()); - readonly onDidChangeActiveViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._onDidChangeActiveViews.event; - - get activeViewDescriptors(): IViewDescriptor[] { - return this.items - .filter(i => i.active) - .map(i => i.viewDescriptor); - } - - get allViewDescriptors(): IViewDescriptor[] { - return this.items.map(i => i.viewDescriptor); - } - - constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, - ) { - super(); - this._register(Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys))(this.onContextChanged, this)); - } - - addViews(viewDescriptors: IViewDescriptor[]): void { - const added: IViewDescriptor[] = []; - - for (const viewDescriptor of viewDescriptors) { - const item = { - viewDescriptor, - active: this.isViewDescriptorActive(viewDescriptor) // TODO: should read from some state? - }; - - this.items.push(item); - - if (viewDescriptor.when) { - for (const key of viewDescriptor.when.keys()) { - this.contextKeys.add(key); - } - } - - if (item.active) { - added.push(viewDescriptor); - } - } - - this._onDidChangeViews.fire({ added: viewDescriptors, removed: [] }); - - if (added.length) { - this._onDidChangeActiveViews.fire({ added, removed: [] }); - } - } - - removeViews(viewDescriptors: IViewDescriptor[]): void { - const removed: IViewDescriptor[] = []; - - for (const viewDescriptor of viewDescriptors) { - const index = firstIndex(this.items, i => i.viewDescriptor.id === viewDescriptor.id); - - if (index === -1) { - continue; - } - - const item = this.items[index]; - this.items.splice(index, 1); - - if (viewDescriptor.when) { - for (const key of viewDescriptor.when.keys()) { - this.contextKeys.delete(key); - } - } - - if (item.active) { - removed.push(viewDescriptor); - } - } - - this._onDidChangeViews.fire({ added: [], removed: viewDescriptors }); - - if (removed.length) { - this._onDidChangeActiveViews.fire({ added: [], removed }); - } - } - - private onContextChanged(event: IContextKeyChangeEvent): void { - const removed: IViewDescriptor[] = []; - const added: IViewDescriptor[] = []; - - for (const item of this.items) { - const active = this.isViewDescriptorActive(item.viewDescriptor); - - if (item.active !== active) { - if (active) { - added.push(item.viewDescriptor); - } else { - removed.push(item.viewDescriptor); - } - } - - item.active = active; - } - - if (added.length || removed.length) { - this._onDidChangeActiveViews.fire({ added, removed }); - } - } - - private isViewDescriptorActive(viewDescriptor: IViewDescriptor): boolean { - return !viewDescriptor.when || this.contextKeyService.contextMatchesRules(viewDescriptor.when); - } -} +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ViewContainerModel } from 'vs/workbench/services/views/common/viewContainerModel'; interface ICachedViewContainerInfo { containerId: string; @@ -189,7 +37,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor private readonly _onDidChangeLocation: Emitter<{ views: IViewDescriptor[], from: ViewContainerLocation, to: ViewContainerLocation }> = this._register(new Emitter<{ views: IViewDescriptor[], from: ViewContainerLocation, to: ViewContainerLocation }>()); readonly onDidChangeLocation: Event<{ views: IViewDescriptor[], from: ViewContainerLocation, to: ViewContainerLocation }> = this._onDidChangeLocation.event; - private readonly viewDescriptorCollections: Map; + private readonly viewContainerModels: Map; private readonly activeViewContextKeys: Map>; private readonly movableViewContextKeys: Map>; private readonly defaultViewLocationContextKeys: Map>; @@ -216,6 +64,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @@ -225,7 +74,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor super(); storageKeysSyncRegistryService.registerStorageKey({ key: ViewDescriptorService.CACHED_VIEW_POSITIONS, version: 1 }); - this.viewDescriptorCollections = new Map(); + this.viewContainerModels = new Map(); this.activeViewContextKeys = new Map>(); this.movableViewContextKeys = new Map>(); this.defaultViewLocationContextKeys = new Map>(); @@ -249,8 +98,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this._register(this.viewContainersRegistry.onDidRegister(({ viewContainer }) => this.onDidRegisterViewContainer(viewContainer))); this._register(this.viewContainersRegistry.onDidDeregister(({ viewContainer }) => this.onDidDeregisterViewContainer(viewContainer))); this._register(toDisposable(() => { - this.viewDescriptorCollections.forEach(({ disposable }) => disposable.dispose()); - this.viewDescriptorCollections.clear(); + this.viewContainerModels.forEach(({ disposable }) => disposable.dispose()); + this.viewContainerModels.clear(); })); this._register(this.storageService.onDidChangeStorage((e) => { this.onDidStorageChange(e); })); @@ -265,7 +114,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor const containerData = groupedViews.get(containerId)!; // The container has not been registered yet - if (!viewContainer || !this.viewDescriptorCollections.has(viewContainer)) { + if (!viewContainer || !this.viewContainerModels.has(viewContainer)) { if (containerData.cachedContainerInfo && this.shouldGenerateContainer(containerData.cachedContainerInfo)) { const containerInfo = containerData.cachedContainerInfo; @@ -287,7 +136,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor const viewContainer = this.viewContainersRegistry.get(viewContainerId); // The container has not been registered yet - if (!viewContainer || !this.viewDescriptorCollections.has(viewContainer)) { + if (!viewContainer || !this.viewContainerModels.has(viewContainer)) { continue; } @@ -410,8 +259,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor return this.viewsRegistry.getViewContainer(viewId) ?? null; } - getViewDescriptors(container: ViewContainer): ViewDescriptorCollection { - return this.getOrRegisterViewDescriptorCollection(container); + getViewContainerModel(container: ViewContainer): ViewContainerModel { + return this.getOrRegisterViewContainerModel(container); } moveViewToLocation(view: IViewDescriptor, location: ViewContainerLocation): void { @@ -492,9 +341,10 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor return this.viewContainersRegistry.registerViewContainer({ id, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [id, `${id}.state`, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [id, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), name: 'Custom Views', // we don't want to see this, so no need to localize icon: location === ViewContainerLocation.Sidebar ? 'codicon-window' : undefined, + storageId: `${id}.state`, hideIfEmpty: true }, location); } @@ -544,8 +394,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor // If a value is not present in the cache, it must be reset to default this.viewContainersRegistry.all.forEach(viewContainer => { - const viewDescriptorCollection = this.getViewDescriptors(viewContainer); - viewDescriptorCollection.allViewDescriptors.forEach(viewDescriptor => { + const viewContainerModel = this.getViewContainerModel(viewContainer); + viewContainerModel.allViewDescriptors.forEach(viewDescriptor => { if (!newCachedPositions.has(viewDescriptor.id)) { const currentContainer = this.getViewContainer(viewDescriptor.id); const defaultContainer = this.getDefaultContainer(viewDescriptor.id); @@ -580,8 +430,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor private saveViewPositionsToCache(): void { this.viewContainersRegistry.all.forEach(viewContainer => { - const viewDescriptorCollection = this.getViewDescriptors(viewContainer); - viewDescriptorCollection.allViewDescriptors.forEach(viewDescriptor => { + const viewContainerModel = this.getViewContainerModel(viewContainer); + viewContainerModel.allViewDescriptors.forEach(viewDescriptor => { const containerLocation = this.getViewContainerLocation(viewContainer); this.cachedViewInfo.set(viewDescriptor.id, { containerId: viewContainer.id, @@ -628,20 +478,20 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } private onDidRegisterViewContainer(viewContainer: ViewContainer): void { - this.getOrRegisterViewDescriptorCollection(viewContainer); + this.getOrRegisterViewContainerModel(viewContainer); } - private getOrRegisterViewDescriptorCollection(viewContainer: ViewContainer): ViewDescriptorCollection { - let viewDescriptorCollection = this.viewDescriptorCollections.get(viewContainer)?.viewDescriptorCollection; + private getOrRegisterViewContainerModel(viewContainer: ViewContainer): ViewContainerModel { + let viewContainerModel = this.viewContainerModels.get(viewContainer)?.viewContainerModel; - if (!viewDescriptorCollection) { + if (!viewContainerModel) { const disposables = new DisposableStore(); - viewDescriptorCollection = disposables.add(new ViewDescriptorCollection(this.contextKeyService)); + viewContainerModel = disposables.add(this.instantiationService.createInstance(ViewContainerModel, viewContainer)); - this.onDidChangeActiveViews({ added: viewDescriptorCollection.activeViewDescriptors, removed: [] }); - viewDescriptorCollection.onDidChangeActiveViews(changed => this.onDidChangeActiveViews(changed), this, disposables); + this.onDidChangeActiveViews({ added: viewContainerModel.activeViewDescriptors, removed: [] }); + viewContainerModel.onDidChangeActiveViewDescriptors(changed => this.onDidChangeActiveViews(changed), this, disposables); - this.viewDescriptorCollections.set(viewContainer, { viewDescriptorCollection, disposable: disposables }); + this.viewContainerModels.set(viewContainer, { viewContainerModel: viewContainerModel, disposable: disposables }); const viewsToRegister = this.getViewsByContainer(viewContainer); if (viewsToRegister.length) { @@ -650,18 +500,18 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } } - return viewDescriptorCollection; + return viewContainerModel; } private onDidDeregisterViewContainer(viewContainer: ViewContainer): void { - const viewDescriptorCollectionItem = this.viewDescriptorCollections.get(viewContainer); - if (viewDescriptorCollectionItem) { - viewDescriptorCollectionItem.disposable.dispose(); - this.viewDescriptorCollections.delete(viewContainer); + const viewContainerModelItem = this.viewContainerModels.get(viewContainer); + if (viewContainerModelItem) { + viewContainerModelItem.disposable.dispose(); + this.viewContainerModels.delete(viewContainer); } } - private onDidChangeActiveViews({ added, removed }: { added: IViewDescriptor[], removed: IViewDescriptor[]; }): void { + private onDidChangeActiveViews({ added, removed }: { added: ReadonlyArray, removed: ReadonlyArray; }): void { added.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(true)); removed.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(false)); } @@ -674,7 +524,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this.getOrCreateDefaultViewLocationContextKey(view).set(this.getDefaultContainer(view.id) === container); }); - this.getViewDescriptors(container).addViews(views); + this.getViewContainerModel(container).add(views); } private removeViews(container: ViewContainer, views: IViewDescriptor[]): void { @@ -682,7 +532,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor views.forEach(view => this.getOrCreateDefaultViewLocationContextKey(view).set(false)); // Remove the views - this.getViewDescriptors(container).removeViews(views); + this.getViewContainerModel(container).remove(views); } private getOrCreateActiveViewContextKey(viewDescriptor: IViewDescriptor): IContextKey { diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts new file mode 100644 index 00000000000..8f6c8e19e0f --- /dev/null +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -0,0 +1,654 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ViewContainer, IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions, IViewContainerModel, IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/common/views'; +import { IContextKeyService, IReadableSet } from 'vs/platform/contextkey/common/contextkey'; +import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; +import { firstIndex, move } from 'vs/base/common/arrays'; +import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; +import { values } from 'vs/base/common/map'; +import { isEqual } from 'vs/base/common/resources'; + +class CounterSet implements IReadableSet { + + private map = new Map(); + + add(value: T): CounterSet { + this.map.set(value, (this.map.get(value) || 0) + 1); + return this; + } + + delete(value: T): boolean { + let counter = this.map.get(value) || 0; + + if (counter === 0) { + return false; + } + + counter--; + + if (counter === 0) { + this.map.delete(value); + } else { + this.map.set(value, counter); + } + + return true; + } + + has(value: T): boolean { + return this.map.has(value); + } +} + +interface IStoredWorkspaceViewState { + collapsed: boolean; + isHidden: boolean; + size?: number; + order?: number; +} + +interface IStoredGlobalViewState { + id: string; + isHidden: boolean; + order?: number; +} + +interface IViewDescriptorState { + visibleGlobal: boolean | undefined; + visibleWorkspace: boolean | undefined; + collapsed: boolean | undefined; + active: boolean + order?: number; + size?: number; +} + +class ViewDescriptorsState extends Disposable { + + private readonly workspaceViewsStateStorageId: string; + private readonly globalViewsStateStorageId: string; + private readonly state: Map; + + private _onDidChangeStoredState = this._register(new Emitter<{ id: string, visible: boolean }[]>()); + readonly onDidChangeStoredState = this._onDidChangeStoredState.event; + + constructor( + viewContainerStorageId: string, + @IStorageService private readonly storageService: IStorageService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService + ) { + super(); + + this.globalViewsStateStorageId = `${viewContainerStorageId}.hidden`; + this.workspaceViewsStateStorageId = viewContainerStorageId; + storageKeysSyncRegistryService.registerStorageKey({ key: this.globalViewsStateStorageId, version: 1 }); + this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); + + this.state = this.initialize(); + } + + set(id: string, state: IViewDescriptorState): void { + this.state.set(id, state); + } + + get(id: string): IViewDescriptorState | undefined { + return this.state.get(id); + } + + updateState(viewDescriptors: ReadonlyArray): void { + this.updateWorkspaceState(viewDescriptors); + this.updateGlobalState(viewDescriptors); + } + + private updateWorkspaceState(viewDescriptors: ReadonlyArray): void { + const storedViewsStates: { [id: string]: IStoredWorkspaceViewState; } = JSON.parse(this.storageService.get(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); + for (const viewDescriptor of viewDescriptors) { + const viewState = this.state.get(viewDescriptor.id); + if (viewState) { + storedViewsStates[viewDescriptor.id] = { + collapsed: !!viewState.collapsed, + isHidden: !viewState.visibleWorkspace, + size: viewState.size, + order: viewDescriptor.workspace && viewState ? viewState.order : undefined + }; + } + } + + if (Object.keys(storedViewsStates).length > 0) { + this.storageService.store(this.workspaceViewsStateStorageId, JSON.stringify(storedViewsStates), StorageScope.WORKSPACE); + } else { + this.storageService.remove(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE); + } + } + + private updateGlobalState(viewDescriptors: ReadonlyArray): void { + const storedGlobalState = this.getStoredGlobalState(); + for (const viewDescriptor of viewDescriptors) { + const state = this.state.get(viewDescriptor.id); + storedGlobalState.set(viewDescriptor.id, { + id: viewDescriptor.id, + isHidden: state && viewDescriptor.canToggleVisibility ? !state.visibleGlobal : false, + order: !viewDescriptor.workspace && state ? state.order : undefined + }); + } + this.setStoredGlobalState(storedGlobalState); + } + + private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { + if (e.key === this.globalViewsStateStorageId && e.scope === StorageScope.GLOBAL + && this.globalViewsStatesValue !== this.getStoredGlobalViewsStatesValue() /* This checks if current window changed the value or not */) { + this._globalViewsStatesValue = undefined; + const storedViewsVisibilityStates = this.getStoredGlobalState(); + const changedStates: { id: string, visible: boolean }[] = []; + for (const [id, storedState] of storedViewsVisibilityStates) { + const state = this.state.get(id); + if (state) { + if (state.visibleGlobal !== !storedState.isHidden) { + changedStates.push({ id, visible: !storedState.isHidden }); + } + } + } + if (changedStates.length) { + this._onDidChangeStoredState.fire(changedStates); + } + } + } + + private initialize(): Map { + const viewStates = new Map(); + const workspaceViewsStates = <{ [id: string]: IStoredWorkspaceViewState; }>JSON.parse(this.storageService.get(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); + for (const id of Object.keys(workspaceViewsStates)) { + const workspaceViewState = workspaceViewsStates[id]; + viewStates.set(id, { + active: false, + visibleGlobal: undefined, + visibleWorkspace: isUndefined(workspaceViewState.isHidden) ? undefined : !workspaceViewState.isHidden, + collapsed: workspaceViewState.collapsed, + order: workspaceViewState.order, + size: workspaceViewState.size, + }); + } + + // Migrate to `viewletStateStorageId` + const value = this.storageService.get(this.globalViewsStateStorageId, StorageScope.WORKSPACE, '[]'); + const { state: workspaceVisibilityStates } = this.parseStoredGlobalState(value); + if (workspaceVisibilityStates.size > 0) { + for (const { id, isHidden } of values(workspaceVisibilityStates)) { + let viewState = viewStates.get(id); + // Not migrated to `viewletStateStorageId` + if (viewState) { + if (isUndefined(viewState.visibleWorkspace)) { + viewState.visibleWorkspace = !isHidden; + } + } else { + viewStates.set(id, { + active: false, + collapsed: undefined, + visibleGlobal: undefined, + visibleWorkspace: !isHidden, + }); + } + } + this.storageService.remove(this.globalViewsStateStorageId, StorageScope.WORKSPACE); + } + + const { state, hasDuplicates } = this.parseStoredGlobalState(this.globalViewsStatesValue); + if (hasDuplicates) { + this.setStoredGlobalState(state); + } + for (const { id, isHidden, order } of values(state)) { + let viewState = viewStates.get(id); + if (viewState) { + viewState.visibleGlobal = !isHidden; + if (!isUndefined(order)) { + viewState.order = order; + } + } else { + viewStates.set(id, { + active: false, + visibleGlobal: !isHidden, + order, + collapsed: undefined, + visibleWorkspace: undefined, + }); + } + } + return viewStates; + } + + private getStoredGlobalState(): Map { + return this.parseStoredGlobalState(this.globalViewsStatesValue).state; + } + + private setStoredGlobalState(storedGlobalState: Map): void { + this.globalViewsStatesValue = JSON.stringify(values(storedGlobalState)); + } + + private parseStoredGlobalState(value: string): { state: Map, hasDuplicates: boolean } { + const storedValue = >JSON.parse(value); + let hasDuplicates = false; + const state = storedValue.reduce((result, storedState) => { + if (typeof storedState === 'string' /* migration */) { + hasDuplicates = hasDuplicates || result.has(storedState); + result.set(storedState, { id: storedState, isHidden: true }); + } else { + hasDuplicates = hasDuplicates || result.has(storedState.id); + result.set(storedState.id, storedState); + } + return result; + }, new Map()); + return { state, hasDuplicates }; + } + + private _globalViewsStatesValue: string | undefined; + private get globalViewsStatesValue(): string { + if (!this._globalViewsStatesValue) { + this._globalViewsStatesValue = this.getStoredGlobalViewsStatesValue(); + } + + return this._globalViewsStatesValue; + } + + private set globalViewsStatesValue(globalViewsStatesValue: string) { + if (this.globalViewsStatesValue !== globalViewsStatesValue) { + this._globalViewsStatesValue = globalViewsStatesValue; + this.setStoredGlobalViewsStatesValue(globalViewsStatesValue); + } + } + + private getStoredGlobalViewsStatesValue(): string { + return this.storageService.get(this.globalViewsStateStorageId, StorageScope.GLOBAL, '[]'); + } + + private setStoredGlobalViewsStatesValue(value: string): void { + this.storageService.store(this.globalViewsStateStorageId, value, StorageScope.GLOBAL); + } + +} + +interface IViewDescriptorItem { + viewDescriptor: IViewDescriptor; + state: IViewDescriptorState; +} + +export class ViewContainerModel extends Disposable implements IViewContainerModel { + + private readonly contextKeys = new CounterSet(); + private viewDescriptorItems: IViewDescriptorItem[] = []; + private viewDescriptorsState: ViewDescriptorsState; + + // Container Info + private _title!: string; + get title(): string { return this._title; } + private _icon: URI | string | undefined; + get icon(): URI | string | undefined { return this._icon; } + + private _onDidChangeContainerInfo = this._register(new Emitter<{ title?: boolean, icon?: boolean }>()); + readonly onDidChangeContainerInfo = this._onDidChangeContainerInfo.event; + + // All View Descriptors + get allViewDescriptors(): ReadonlyArray { return this.viewDescriptorItems.map(item => item.viewDescriptor); } + private _onDidChangeAllViewDescriptors = this._register(new Emitter<{ added: ReadonlyArray, removed: ReadonlyArray }>()); + readonly onDidChangeAllViewDescriptors = this._onDidChangeAllViewDescriptors.event; + + // Active View Descriptors + get activeViewDescriptors(): ReadonlyArray { return this.viewDescriptorItems.filter(item => item.state.active).map(item => item.viewDescriptor); } + private _onDidChangeActiveViewDescriptors = this._register(new Emitter<{ added: ReadonlyArray, removed: ReadonlyArray }>()); + readonly onDidChangeActiveViewDescriptors = this._onDidChangeActiveViewDescriptors.event; + + // Visible View Descriptors + get visibleViewDescriptors(): ReadonlyArray { return this.viewDescriptorItems.filter(item => this.isViewDescriptorVisible(item)).map(item => item.viewDescriptor); } + + private _onDidAddVisibleViewDescriptors = this._register(new Emitter()); + readonly onDidAddVisibleViewDescriptors: Event = this._onDidAddVisibleViewDescriptors.event; + + private _onDidRemoveVisibleViewDescriptors = this._register(new Emitter()); + readonly onDidRemoveVisibleViewDescriptors: Event = this._onDidRemoveVisibleViewDescriptors.event; + + private _onDidMoveVisibleViewDescriptors = this._register(new Emitter<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }>()); + readonly onDidMoveVisibleViewDescriptors: Event<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }> = this._onDidMoveVisibleViewDescriptors.event; + + constructor( + private readonly container: ViewContainer, + @IInstantiationService instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(); + + this._register(Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys))(() => this.onDidChangeContext())); + this.viewDescriptorsState = this._register(instantiationService.createInstance(ViewDescriptorsState, container.storageId || `${container.id}.state`)); + this._register(this.viewDescriptorsState.onDidChangeStoredState(items => this.updateVisibility(items))); + + this._register(Event.any( + this.onDidAddVisibleViewDescriptors, + this.onDidRemoveVisibleViewDescriptors, + this.onDidMoveVisibleViewDescriptors) + (() => { + this.viewDescriptorsState.updateState(this.allViewDescriptors); + this.updateContainerInfo(); + })); + + this.updateContainerInfo(); + } + + private updateContainerInfo(): void { + /* Use default container info if one of the visible view descriptors belongs to the current container by default */ + const useDefaultContainerInfo = this.visibleViewDescriptors.length === 0 || this.visibleViewDescriptors.some(v => Registry.as(ViewExtensions.ViewsRegistry).getViewContainer(v.id) === this.container); + const title = useDefaultContainerInfo ? this.container.name : this.visibleViewDescriptors[0]?.name || ''; + let titleChanged: boolean = false; + if (this._title !== title) { + this._title = title; + titleChanged = true; + } + + const icon = useDefaultContainerInfo ? this.container.icon : this.visibleViewDescriptors[0]?.containerIcon || 'codicon-window'; + let iconChanged: boolean = false; + if (URI.isUri(icon) && URI.isUri(this._icon) ? isEqual(icon, this._icon) : this._icon !== icon) { + this._icon = icon; + iconChanged = true; + } + + if (titleChanged || iconChanged) { + this._onDidChangeContainerInfo.fire({ title: titleChanged, icon: iconChanged }); + } + } + + isVisible(id: string): boolean { + const viewDescriptorItem = this.viewDescriptorItems.filter(v => v.viewDescriptor.id === id)[0]; + if (!viewDescriptorItem) { + throw new Error(`Unknown view ${id}`); + } + return this.isViewDescriptorVisible(viewDescriptorItem); + } + + setVisible(id: string, visible: boolean, size?: number): void { + this.updateVisibility([{ id, visible, size }]); + } + + private updateVisibility(viewDescriptors: { id: string, visible: boolean, size?: number }[]): void { + const added: IAddedViewDescriptorRef[] = []; + const removed: IViewDescriptorRef[] = []; + + for (const { visibleIndex, viewDescriptorItem, visible, size } of viewDescriptors.map(({ id, visible, size }) => ({ ...this.find(id), visible, size }))) { + const viewDescriptor = viewDescriptorItem.viewDescriptor; + + if (!viewDescriptor.canToggleVisibility) { + throw new Error(`Can't toggle this view's visibility`); + } + + if (this.isViewDescriptorVisible(viewDescriptorItem) === visible) { + return; + } + + if (viewDescriptor.workspace) { + viewDescriptorItem.state.visibleWorkspace = visible; + } else { + viewDescriptorItem.state.visibleGlobal = visible; + } + + if (typeof viewDescriptorItem.state.size === 'number') { + viewDescriptorItem.state.size = size; + } + + if (visible) { + added.push({ index: visibleIndex, viewDescriptor, size: viewDescriptorItem.state.size, collapsed: !!viewDescriptorItem.state.collapsed }); + } else { + removed.push({ index: visibleIndex, viewDescriptor }); + } + } + + if (added.length) { + this._onDidAddVisibleViewDescriptors.fire(added); + } + if (removed.length) { + this._onDidRemoveVisibleViewDescriptors.fire(removed); + } + } + + isCollapsed(id: string): boolean { + return !!this.find(id).viewDescriptorItem.state.collapsed; + } + + setCollapsed(id: string, collapsed: boolean): void { + const { viewDescriptorItem } = this.find(id); + if (viewDescriptorItem.state.collapsed !== collapsed) { + viewDescriptorItem.state.collapsed = collapsed; + } + this.viewDescriptorsState.updateState(this.allViewDescriptors); + } + + getSize(id: string): number | undefined { + return this.find(id).viewDescriptorItem.state.size; + } + + setSize(id: string, size: number): void { + const { viewDescriptorItem } = this.find(id); + if (viewDescriptorItem.state.size !== size) { + viewDescriptorItem.state.size = size; + } + this.viewDescriptorsState.updateState(this.allViewDescriptors); + } + + move(from: string, to: string): void { + const fromIndex = firstIndex(this.viewDescriptorItems, v => v.viewDescriptor.id === from); + const toIndex = firstIndex(this.viewDescriptorItems, v => v.viewDescriptor.id === to); + + const fromViewDescriptor = this.viewDescriptorItems[fromIndex]; + const toViewDescriptor = this.viewDescriptorItems[toIndex]; + + move(this.viewDescriptorItems, fromIndex, toIndex); + + for (let index = 0; index < this.viewDescriptorItems.length; index++) { + this.viewDescriptorItems[index].state.order = index; + } + + this._onDidMoveVisibleViewDescriptors.fire({ + from: { index: fromIndex, viewDescriptor: fromViewDescriptor.viewDescriptor }, + to: { index: toIndex, viewDescriptor: toViewDescriptor.viewDescriptor } + }); + } + + add(viewDescriptors: IViewDescriptor[]): void { + const addedItems: IViewDescriptorItem[] = []; + const addedActiveDescriptors: IViewDescriptor[] = []; + const addedVisibleItems: { index: number, viewDescriptor: IViewDescriptor, size?: number, collapsed: boolean; }[] = []; + + for (const viewDescriptor of viewDescriptors) { + + if (viewDescriptor.when) { + for (const key of viewDescriptor.when.keys()) { + this.contextKeys.add(key); + } + } + + let state = this.viewDescriptorsState.get(viewDescriptor.id); + if (state) { + // set defaults if not set + if (viewDescriptor.workspace) { + state.visibleWorkspace = isUndefinedOrNull(state.visibleWorkspace) ? !viewDescriptor.hideByDefault : state.visibleWorkspace; + } else { + state.visibleGlobal = isUndefinedOrNull(state.visibleGlobal) ? !viewDescriptor.hideByDefault : state.visibleGlobal; + } + state.collapsed = isUndefinedOrNull(state.collapsed) ? !!viewDescriptor.collapsed : state.collapsed; + } else { + state = { + active: false, + visibleGlobal: !viewDescriptor.hideByDefault, + visibleWorkspace: !viewDescriptor.hideByDefault, + collapsed: !!viewDescriptor.collapsed, + }; + } + this.viewDescriptorsState.set(viewDescriptor.id, state); + state.active = this.contextKeyService.contextMatchesRules(viewDescriptor.when); + addedItems.push({ viewDescriptor, state }); + + if (state.active) { + addedActiveDescriptors.push(viewDescriptor); + } + } + + this.viewDescriptorItems.push(...addedItems); + this.viewDescriptorItems.sort(this.compareViewDescriptors.bind(this)); + + for (const viewDescriptorItem of addedItems) { + if (this.isViewDescriptorVisible(viewDescriptorItem)) { + const { visibleIndex } = this.find(viewDescriptorItem.viewDescriptor.id); + addedVisibleItems.push({ index: visibleIndex, viewDescriptor: viewDescriptorItem.viewDescriptor, size: viewDescriptorItem.state.size, collapsed: !!viewDescriptorItem.state.collapsed }); + } + } + + this._onDidChangeAllViewDescriptors.fire({ added: addedItems.map(({ viewDescriptor }) => viewDescriptor), removed: [] }); + if (addedActiveDescriptors.length) { + this._onDidChangeActiveViewDescriptors.fire(({ added: addedActiveDescriptors, removed: [] })); + } + if (addedVisibleItems.length) { + this._onDidAddVisibleViewDescriptors.fire(addedVisibleItems); + } + } + + remove(viewDescriptors: IViewDescriptor[]): void { + const removed: IViewDescriptor[] = []; + const removedItems: IViewDescriptorItem[] = []; + const removedActiveDescriptors: IViewDescriptor[] = []; + const removedVisibleItems: { index: number, viewDescriptor: IViewDescriptor; }[] = []; + + for (const viewDescriptor of viewDescriptors) { + if (viewDescriptor.when) { + for (const key of viewDescriptor.when.keys()) { + this.contextKeys.delete(key); + } + } + const index = firstIndex(this.viewDescriptorItems, i => i.viewDescriptor.id === viewDescriptor.id); + if (index !== -1) { + removed.push(viewDescriptor); + const viewDescriptorItem = this.viewDescriptorItems[index]; + if (viewDescriptorItem.state.active) { + removedActiveDescriptors.push(viewDescriptorItem.viewDescriptor); + } + if (this.isViewDescriptorVisible(viewDescriptorItem)) { + const { visibleIndex } = this.find(viewDescriptorItem.viewDescriptor.id); + removedVisibleItems.push({ index: visibleIndex, viewDescriptor: viewDescriptorItem.viewDescriptor }); + } + removedItems.push(viewDescriptorItem); + } + } + + removedItems.forEach(item => this.viewDescriptorItems.splice(this.viewDescriptorItems.indexOf(item), 1)); + + this._onDidChangeAllViewDescriptors.fire({ added: [], removed }); + if (removedActiveDescriptors.length) { + this._onDidChangeActiveViewDescriptors.fire(({ added: [], removed: removedActiveDescriptors })); + } + if (removedVisibleItems.length) { + this._onDidRemoveVisibleViewDescriptors.fire(removedVisibleItems); + } + } + + private onDidChangeContext(): void { + const addedActiveItems: { item: IViewDescriptorItem, wasVisible: boolean }[] = []; + const removedActiveItems: { item: IViewDescriptorItem, wasVisible: boolean }[] = []; + const removedVisibleItems: { index: number, viewDescriptor: IViewDescriptor; }[] = []; + const addedVisibleItems: { index: number, viewDescriptor: IViewDescriptor, size?: number, collapsed: boolean; }[] = []; + + for (const item of this.viewDescriptorItems) { + const wasActive = item.state.active; + const wasVisible = this.isViewDescriptorVisible(item); + const isActive = this.contextKeyService.contextMatchesRules(item.viewDescriptor.when); + if (wasActive !== isActive) { + if (isActive) { + addedActiveItems.push({ item, wasVisible }); + } else { + removedActiveItems.push({ item, wasVisible }); + } + } + } + + for (const { item, wasVisible } of removedActiveItems) { + if (wasVisible) { + const { visibleIndex } = this.find(item.viewDescriptor.id); + removedVisibleItems.push({ index: visibleIndex, viewDescriptor: item.viewDescriptor }); + } + } + + // Update the State + removedActiveItems.forEach(({ item }) => item.state.active = false); + addedActiveItems.forEach(({ item }) => item.state.active = true); + + for (const { item, wasVisible } of addedActiveItems) { + if (wasVisible !== this.isViewDescriptorVisibleWhenActive(item)) { + const { visibleIndex } = this.find(item.viewDescriptor.id); + addedVisibleItems.push({ index: visibleIndex, viewDescriptor: item.viewDescriptor, size: item.state.size, collapsed: !!item.state.collapsed }); + } + } + + if (addedActiveItems.length || removedActiveItems.length) { + this._onDidChangeActiveViewDescriptors.fire(({ added: addedActiveItems.map(({ item }) => item.viewDescriptor), removed: removedActiveItems.map(({ item }) => item.viewDescriptor) })); + } + if (removedVisibleItems.length) { + this._onDidRemoveVisibleViewDescriptors.fire(removedVisibleItems); + } + if (addedVisibleItems.length) { + this._onDidAddVisibleViewDescriptors.fire(addedVisibleItems); + } + } + + private isViewDescriptorVisible(viewDescriptorItem: IViewDescriptorItem): boolean { + if (!viewDescriptorItem.state.active) { + return false; + } + return this.isViewDescriptorVisibleWhenActive(viewDescriptorItem); + } + + private isViewDescriptorVisibleWhenActive(viewDescriptorItem: IViewDescriptorItem): boolean { + if (viewDescriptorItem.viewDescriptor.workspace) { + return !!viewDescriptorItem.state.visibleWorkspace; + } + return !!viewDescriptorItem.state.visibleGlobal; + } + + private find(id: string): { index: number, visibleIndex: number, viewDescriptorItem: IViewDescriptorItem; } { + for (let i = 0, visibleIndex = 0; i < this.viewDescriptorItems.length; i++) { + const viewDescriptorItem = this.viewDescriptorItems[i]; + if (viewDescriptorItem.viewDescriptor.id === id) { + return { index: i, visibleIndex, viewDescriptorItem: viewDescriptorItem }; + } + if (this.isViewDescriptorVisible(viewDescriptorItem)) { + visibleIndex++; + } + } + throw new Error(`view descriptor ${id} not found`); + } + + private compareViewDescriptors(a: IViewDescriptorItem, b: IViewDescriptorItem): number { + if (a.viewDescriptor.id === b.viewDescriptor.id) { + return 0; + } + + return (this.getViewOrder(a) - this.getViewOrder(b)) || this.getGroupOrderResult(a.viewDescriptor, b.viewDescriptor); + } + + private getViewOrder(viewDescriptorItem: IViewDescriptorItem): number { + const viewOrder = typeof viewDescriptorItem.state.order === 'number' ? viewDescriptorItem.state.order : viewDescriptorItem.viewDescriptor.order; + return typeof viewOrder === 'number' ? viewOrder : Number.MAX_VALUE; + } + + private getGroupOrderResult(a: IViewDescriptor, b: IViewDescriptor) { + if (!a.group || !b.group) { + return 0; + } + + if (a.group === b.group) { + return 0; + } + + return a.group < b.group ? -1 : 1; + } +} diff --git a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts new file mode 100644 index 00000000000..e6d4fd83eab --- /dev/null +++ b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts @@ -0,0 +1,385 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewContainerModel, IViewDescriptorService, ViewContainer } from 'vs/workbench/common/views'; +import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { move } from 'vs/base/common/arrays'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; +import { ViewDescriptorService } from 'vs/workbench/services/views/browser/viewDescriptorService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; + +const ViewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); +const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); + +class ViewDescriptorSequence { + + readonly elements: IViewDescriptor[]; + private disposables: IDisposable[] = []; + + constructor(model: IViewContainerModel) { + this.elements = [...model.visibleViewDescriptors]; + model.onDidAddVisibleViewDescriptors(added => added.forEach(({ viewDescriptor, index }) => this.elements.splice(index, 0, viewDescriptor)), null, this.disposables); + model.onDidRemoveVisibleViewDescriptors(removed => removed.sort((a, b) => b.index - a.index).forEach(({ index }) => this.elements.splice(index, 1)), null, this.disposables); + model.onDidMoveVisibleViewDescriptors(({ from, to }) => move(this.elements, from.index, to.index), null, this.disposables); + } + + dispose() { + this.disposables = dispose(this.disposables); + } +} + +suite('ViewContainerModel', () => { + + let container: ViewContainer; + let disposableStore: DisposableStore; + let contextKeyService: IContextKeyService; + let viewDescriptorService: IViewDescriptorService; + let storageService: IStorageService; + + setup(() => { + disposableStore = new DisposableStore(); + const instantiationService: TestInstantiationService = workbenchInstantiationService(); + contextKeyService = instantiationService.createInstance(ContextKeyService); + instantiationService.stub(IContextKeyService, contextKeyService); + storageService = instantiationService.get(IStorageService); + viewDescriptorService = instantiationService.createInstance(ViewDescriptorService); + }); + + teardown(() => { + disposableStore.dispose(); + ViewsRegistry.deregisterViews(ViewsRegistry.getViews(container), container); + ViewContainerRegistry.deregisterViewContainer(container); + }); + + test('empty model', function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + assert.equal(testObject.visibleViewDescriptors.length, 0); + }); + + test('register/unregister', () => { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1' + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.equal(testObject.visibleViewDescriptors.length, 1); + assert.equal(target.elements.length, 1); + assert.deepEqual(testObject.visibleViewDescriptors[0], viewDescriptor); + assert.deepEqual(target.elements[0], viewDescriptor); + + ViewsRegistry.deregisterViews([viewDescriptor], container); + + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + }); + + test('when contexts', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true) + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should not appear since context isnt in'); + assert.equal(target.elements.length, 0); + + const key = contextKeyService.createKey('showview1', false); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should still not appear since showview1 isnt true'); + assert.equal(target.elements.length, 0); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.equal(testObject.visibleViewDescriptors.length, 1, 'view should appear'); + assert.equal(target.elements.length, 1); + assert.deepEqual(testObject.visibleViewDescriptors[0], viewDescriptor); + assert.equal(target.elements[0], viewDescriptor); + + key.set(false); + await new Promise(c => setTimeout(c, 30)); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should disappear'); + assert.equal(target.elements.length, 0); + + ViewsRegistry.deregisterViews([viewDescriptor], container); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should not be there anymore'); + assert.equal(target.elements.length, 0); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should not be there anymore'); + assert.equal(target.elements.length, 0); + }); + + test('when contexts - multiple', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1' }; + const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2', when: ContextKeyExpr.equals('showview2', true) }; + + ViewsRegistry.registerViews([view1, view2], container); + assert.deepEqual(testObject.visibleViewDescriptors, [view1], 'only view1 should be visible'); + assert.deepEqual(target.elements, [view1], 'only view1 should be visible'); + + const key = contextKeyService.createKey('showview2', false); + assert.deepEqual(testObject.visibleViewDescriptors, [view1], 'still only view1 should be visible'); + assert.deepEqual(target.elements, [view1], 'still only view1 should be visible'); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view2], 'both views should be visible'); + assert.deepEqual(target.elements, [view1, view2], 'both views should be visible'); + + ViewsRegistry.deregisterViews([view1, view2], container); + }); + + test('when contexts - multiple 2', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1', when: ContextKeyExpr.equals('showview1', true) }; + const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2' }; + + ViewsRegistry.registerViews([view1, view2], container); + assert.deepEqual(testObject.visibleViewDescriptors, [view2], 'only view2 should be visible'); + assert.deepEqual(target.elements, [view2], 'only view2 should be visible'); + + const key = contextKeyService.createKey('showview1', false); + assert.deepEqual(testObject.visibleViewDescriptors, [view2], 'still only view2 should be visible'); + assert.deepEqual(target.elements, [view2], 'still only view2 should be visible'); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view2], 'both views should be visible'); + assert.deepEqual(target.elements, [view1, view2], 'both views should be visible'); + + ViewsRegistry.deregisterViews([view1, view2], container); + }); + + test('setVisible', () => { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1', canToggleVisibility: true }; + const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2', canToggleVisibility: true }; + const view3: IViewDescriptor = { id: 'view3', ctorDescriptor: null!, name: 'Test View 3', canToggleVisibility: true }; + + ViewsRegistry.registerViews([view1, view2, view3], container); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view2, view3]); + assert.deepEqual(target.elements, [view1, view2, view3]); + + testObject.setVisible('view2', true); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view2, view3], 'nothing should happen'); + assert.deepEqual(target.elements, [view1, view2, view3]); + + testObject.setVisible('view2', false); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view3], 'view2 should hide'); + assert.deepEqual(target.elements, [view1, view3]); + + testObject.setVisible('view1', false); + assert.deepEqual(testObject.visibleViewDescriptors, [view3], 'view1 should hide'); + assert.deepEqual(target.elements, [view3]); + + testObject.setVisible('view3', false); + assert.deepEqual(testObject.visibleViewDescriptors, [], 'view3 shoud hide'); + assert.deepEqual(target.elements, []); + + testObject.setVisible('view1', true); + assert.deepEqual(testObject.visibleViewDescriptors, [view1], 'view1 should show'); + assert.deepEqual(target.elements, [view1]); + + testObject.setVisible('view3', true); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view3], 'view3 should show'); + assert.deepEqual(target.elements, [view1, view3]); + + testObject.setVisible('view2', true); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view2, view3], 'view2 should show'); + assert.deepEqual(target.elements, [view1, view2, view3]); + + ViewsRegistry.deregisterViews([view1, view2, view3], container); + assert.deepEqual(testObject.visibleViewDescriptors, []); + assert.deepEqual(target.elements, []); + }); + + test('move', () => { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1' }; + const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2' }; + const view3: IViewDescriptor = { id: 'view3', ctorDescriptor: null!, name: 'Test View 3' }; + + ViewsRegistry.registerViews([view1, view2, view3], container); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view2, view3], 'model views should be OK'); + assert.deepEqual(target.elements, [view1, view2, view3], 'sql views should be OK'); + + testObject.move('view3', 'view1'); + assert.deepEqual(testObject.visibleViewDescriptors, [view3, view1, view2], 'view3 should go to the front'); + assert.deepEqual(target.elements, [view3, view1, view2]); + + testObject.move('view1', 'view2'); + assert.deepEqual(testObject.visibleViewDescriptors, [view3, view2, view1], 'view1 should go to the end'); + assert.deepEqual(target.elements, [view3, view2, view1]); + + testObject.move('view1', 'view3'); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view3, view2], 'view1 should go to the front'); + assert.deepEqual(target.elements, [view1, view3, view2]); + + testObject.move('view2', 'view3'); + assert.deepEqual(testObject.visibleViewDescriptors, [view1, view2, view3], 'view2 should go to the middle'); + assert.deepEqual(target.elements, [view1, view2, view3]); + }); + + test('view states', async function () { + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL); + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1' + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should not appear since it was set not visible in view state'); + assert.equal(target.elements.length, 0); + }); + + test('view states and when contexts', async function () { + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL); + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true) + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should not appear since context isnt in'); + assert.equal(target.elements.length, 0); + + const key = contextKeyService.createKey('showview1', false); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should still not appear since showview1 isnt true'); + assert.equal(target.elements.length, 0); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should still not appear since it was set not visible in view state'); + assert.equal(target.elements.length, 0); + }); + + test('view states and when contexts multiple views', async function () { + storageService.store(`${container.id}.state.hidden`, JSON.stringify([{ id: 'view1', isHidden: true }]), StorageScope.GLOBAL); + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + + assert.equal(testObject.visibleViewDescriptors.length, 0); + assert.equal(target.elements.length, 0); + + const view1: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview', true) + }; + const view2: IViewDescriptor = { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + }; + const view3: IViewDescriptor = { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + when: ContextKeyExpr.equals('showview', true) + }; + + ViewsRegistry.registerViews([view1, view2, view3], container); + assert.deepEqual(testObject.visibleViewDescriptors, [view2], 'Only view2 should be visible'); + assert.deepEqual(target.elements, [view2]); + + const key = contextKeyService.createKey('showview', false); + assert.deepEqual(testObject.visibleViewDescriptors, [view2], 'Only view2 should be visible'); + assert.deepEqual(target.elements, [view2]); + + key.set(true); + await new Promise(c => setTimeout(c, 30)); + assert.deepEqual(testObject.visibleViewDescriptors, [view2, view3], 'view3 should be visible'); + assert.deepEqual(target.elements, [view2, view3]); + + key.set(false); + await new Promise(c => setTimeout(c, 30)); + assert.deepEqual(testObject.visibleViewDescriptors, [view2], 'Only view2 should be visible'); + assert.deepEqual(target.elements, [view2]); + }); + + test('remove event is not triggered if view was hidden and removed', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + when: ContextKeyExpr.equals('showview1', true), + canToggleVisibility: true + }; + + ViewsRegistry.registerViews([viewDescriptor], container); + + const key = contextKeyService.createKey('showview1', true); + await new Promise(c => setTimeout(c, 30)); + assert.equal(testObject.visibleViewDescriptors.length, 1, 'view should appear after context is set'); + assert.equal(target.elements.length, 1); + + testObject.setVisible('view1', false); + assert.equal(testObject.visibleViewDescriptors.length, 0, 'view should disappear after setting visibility to false'); + assert.equal(target.elements.length, 0); + + const targetEvent = sinon.spy(testObject.onDidRemoveVisibleViewDescriptors); + key.set(false); + await new Promise(c => setTimeout(c, 30)); + assert.ok(!targetEvent.called, 'remove event should not be called since it is already hidden'); + }); + +}); diff --git a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts new file mode 100644 index 00000000000..7b2f470bc49 --- /dev/null +++ b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts @@ -0,0 +1,268 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewDescriptorService, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ViewDescriptorService } from 'vs/workbench/services/views/browser/viewDescriptorService'; +import { assertIsDefined } from 'vs/base/common/types'; +import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; + +const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); +const sidebarContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: 'testSidebar', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); +const panelContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: 'testPanel', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Panel); + +suite('ViewDescriptorService', () => { + + let viewDescriptorService: IViewDescriptorService; + + setup(() => { + const instantiationService: TestInstantiationService = workbenchInstantiationService(); + instantiationService.stub(IContextKeyService, instantiationService.createInstance(ContextKeyService)); + viewDescriptorService = instantiationService.createInstance(ViewDescriptorService); + }); + + teardown(() => { + ViewsRegistry.deregisterViews(ViewsRegistry.getViews(sidebarContainer), sidebarContainer); + ViewsRegistry.deregisterViews(ViewsRegistry.getViews(panelContainer), panelContainer); + }); + + test('Empty Containers', function () { + const sidebarViews = viewDescriptorService.getViewContainerModel(sidebarContainer); + const panelViews = viewDescriptorService.getViewContainerModel(panelContainer); + assert.equal(sidebarViews.allViewDescriptors.length, 0, 'The sidebar container should have no views yet.'); + assert.equal(panelViews.allViewDescriptors.length, 0, 'The panel container should have no views yet.'); + }); + + test('Register/Deregister', () => { + const viewDescriptors: IViewDescriptor[] = [ + { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canMoveView: true + }, + { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + canMoveView: true + }, + { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + canMoveView: true + } + ]; + + + ViewsRegistry.registerViews(viewDescriptors.slice(0, 2), sidebarContainer); + ViewsRegistry.registerViews(viewDescriptors.slice(2), panelContainer); + + + let sidebarViews = viewDescriptorService.getViewContainerModel(sidebarContainer); + let panelViews = viewDescriptorService.getViewContainerModel(panelContainer); + + assert.equal(sidebarViews.activeViewDescriptors.length, 2, 'Sidebar should have 2 views'); + assert.equal(panelViews.activeViewDescriptors.length, 1, 'Panel should have 1 view'); + + ViewsRegistry.deregisterViews(viewDescriptors.slice(0, 2), sidebarContainer); + ViewsRegistry.deregisterViews(viewDescriptors.slice(2), panelContainer); + + + sidebarViews = viewDescriptorService.getViewContainerModel(sidebarContainer); + panelViews = viewDescriptorService.getViewContainerModel(panelContainer); + + assert.equal(sidebarViews.activeViewDescriptors.length, 0, 'Sidebar should have no views'); + assert.equal(panelViews.activeViewDescriptors.length, 0, 'Panel should have no views'); + }); + + test('move views to existing containers', async function () { + const viewDescriptors: IViewDescriptor[] = [ + { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canMoveView: true + }, + { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + canMoveView: true + }, + { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + canMoveView: true + } + ]; + + ViewsRegistry.registerViews(viewDescriptors.slice(0, 2), sidebarContainer); + ViewsRegistry.registerViews(viewDescriptors.slice(2), panelContainer); + + viewDescriptorService.moveViewsToContainer(viewDescriptors.slice(2), sidebarContainer); + viewDescriptorService.moveViewsToContainer(viewDescriptors.slice(0, 2), panelContainer); + + let sidebarViews = viewDescriptorService.getViewContainerModel(sidebarContainer); + let panelViews = viewDescriptorService.getViewContainerModel(panelContainer); + + assert.equal(sidebarViews.activeViewDescriptors.length, 1, 'Sidebar should have 2 views'); + assert.equal(panelViews.activeViewDescriptors.length, 2, 'Panel should have 1 view'); + + assert.notEqual(sidebarViews.activeViewDescriptors.indexOf(viewDescriptors[2]), -1, `Sidebar should have ${viewDescriptors[2].name}`); + assert.notEqual(panelViews.activeViewDescriptors.indexOf(viewDescriptors[0]), -1, `Panel should have ${viewDescriptors[0].name}`); + assert.notEqual(panelViews.activeViewDescriptors.indexOf(viewDescriptors[1]), -1, `Panel should have ${viewDescriptors[1].name}`); + }); + + test('move views to generated containers', async function () { + const viewDescriptors: IViewDescriptor[] = [ + { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canMoveView: true + }, + { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + canMoveView: true + }, + { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + canMoveView: true + } + ]; + + ViewsRegistry.registerViews(viewDescriptors.slice(0, 2), sidebarContainer); + ViewsRegistry.registerViews(viewDescriptors.slice(2), panelContainer); + + viewDescriptorService.moveViewToLocation(viewDescriptors[0], ViewContainerLocation.Panel); + viewDescriptorService.moveViewToLocation(viewDescriptors[2], ViewContainerLocation.Sidebar); + + let sidebarViews = viewDescriptorService.getViewContainerModel(sidebarContainer); + let panelViews = viewDescriptorService.getViewContainerModel(panelContainer); + + assert.equal(sidebarViews.activeViewDescriptors.length, 1, 'Sidebar container should have 1 view'); + assert.equal(panelViews.activeViewDescriptors.length, 0, 'Panel container should have no views'); + + const generatedPanel = assertIsDefined(viewDescriptorService.getViewContainer(viewDescriptors[0].id)); + const generatedSidebar = assertIsDefined(viewDescriptorService.getViewContainer(viewDescriptors[2].id)); + + assert.equal(viewDescriptorService.getViewContainerLocation(generatedPanel), ViewContainerLocation.Panel, 'Generated Panel should be in located in the panel'); + assert.equal(viewDescriptorService.getViewContainerLocation(generatedSidebar), ViewContainerLocation.Sidebar, 'Generated Sidebar should be in located in the sidebar'); + + assert.equal(viewDescriptorService.getViewContainerLocation(generatedPanel), viewDescriptorService.getViewLocation(viewDescriptors[0].id), 'Panel view location and container location should match'); + assert.equal(viewDescriptorService.getViewContainerLocation(generatedSidebar), viewDescriptorService.getViewLocation(viewDescriptors[2].id), 'Sidebar view location and container location should match'); + + assert.equal(viewDescriptorService.getDefaultContainer(viewDescriptors[2].id), panelContainer, `${viewDescriptors[2].name} has wrong default container`); + assert.equal(viewDescriptorService.getDefaultContainer(viewDescriptors[0].id), sidebarContainer, `${viewDescriptors[0].name} has wrong default container`); + + viewDescriptorService.moveViewToLocation(viewDescriptors[0], ViewContainerLocation.Sidebar); + viewDescriptorService.moveViewToLocation(viewDescriptors[2], ViewContainerLocation.Panel); + + sidebarViews = viewDescriptorService.getViewContainerModel(sidebarContainer); + panelViews = viewDescriptorService.getViewContainerModel(panelContainer); + + assert.equal(sidebarViews.activeViewDescriptors.length, 1, 'Sidebar should have 2 views'); + assert.equal(panelViews.activeViewDescriptors.length, 0, 'Panel should have 1 view'); + + assert.equal(viewDescriptorService.getViewLocation(viewDescriptors[0].id), ViewContainerLocation.Sidebar, 'View should be located in the sidebar'); + assert.equal(viewDescriptorService.getViewLocation(viewDescriptors[2].id), ViewContainerLocation.Panel, 'View should be located in the panel'); + }); + + test('move view events', async function () { + const viewDescriptors: IViewDescriptor[] = [ + { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canMoveView: true + }, + { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + canMoveView: true + }, + { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + canMoveView: true + } + ]; + + + let expectedSequence = ''; + let actualSequence = ''; + const disposables = []; + + const containerMoveString = (view: IViewDescriptor, from: ViewContainer, to: ViewContainer) => { + return `Moved ${view.id} from ${from.id} to ${to.id}\n`; + }; + + const locationMoveString = (view: IViewDescriptor, from: ViewContainerLocation, to: ViewContainerLocation) => { + return `Moved ${view.id} from ${from === ViewContainerLocation.Sidebar ? 'Sidebar' : 'Panel'} to ${to === ViewContainerLocation.Sidebar ? 'Sidebar' : 'Panel'}\n`; + }; + disposables.push(viewDescriptorService.onDidChangeContainer(({ views, from, to }) => { + views.forEach(view => { + actualSequence += containerMoveString(view, from, to); + }); + })); + + disposables.push(viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + views.forEach(view => { + actualSequence += locationMoveString(view, from, to); + }); + })); + + ViewsRegistry.registerViews(viewDescriptors.slice(0, 2), sidebarContainer); + ViewsRegistry.registerViews(viewDescriptors.slice(2), panelContainer); + + expectedSequence += locationMoveString(viewDescriptors[0], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); + viewDescriptorService.moveViewToLocation(viewDescriptors[0], ViewContainerLocation.Panel); + expectedSequence += containerMoveString(viewDescriptors[0], sidebarContainer, viewDescriptorService.getViewContainer(viewDescriptors[0].id)!); + + expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Panel, ViewContainerLocation.Sidebar); + viewDescriptorService.moveViewToLocation(viewDescriptors[2], ViewContainerLocation.Sidebar); + expectedSequence += containerMoveString(viewDescriptors[2], panelContainer, viewDescriptorService.getViewContainer(viewDescriptors[2].id)!); + + + expectedSequence += locationMoveString(viewDescriptors[0], ViewContainerLocation.Panel, ViewContainerLocation.Sidebar); + expectedSequence += containerMoveString(viewDescriptors[0], viewDescriptorService.getViewContainer(viewDescriptors[0].id)!, sidebarContainer); + viewDescriptorService.moveViewsToContainer([viewDescriptors[0]], sidebarContainer); + + expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); + expectedSequence += containerMoveString(viewDescriptors[2], viewDescriptorService.getViewContainer(viewDescriptors[2].id)!, panelContainer); + viewDescriptorService.moveViewsToContainer([viewDescriptors[2]], panelContainer); + + expectedSequence += locationMoveString(viewDescriptors[0], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); + expectedSequence += containerMoveString(viewDescriptors[0], sidebarContainer, panelContainer); + viewDescriptorService.moveViewsToContainer([viewDescriptors[0]], panelContainer); + + expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Panel, ViewContainerLocation.Sidebar); + expectedSequence += containerMoveString(viewDescriptors[2], panelContainer, sidebarContainer); + viewDescriptorService.moveViewsToContainer([viewDescriptors[2]], sidebarContainer); + + expectedSequence += locationMoveString(viewDescriptors[1], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); + expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); + expectedSequence += containerMoveString(viewDescriptors[1], sidebarContainer, panelContainer); + expectedSequence += containerMoveString(viewDescriptors[2], sidebarContainer, panelContainer); + viewDescriptorService.moveViewsToContainer([viewDescriptors[1], viewDescriptors[2]], panelContainer); + + assert.equal(actualSequence, expectedSequence, 'Event sequence not matching expected sequence'); + }); + +}); diff --git a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts index 768e9528a37..f40f7e9c29b 100644 --- a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts @@ -585,6 +585,33 @@ suite('ExtHostLanguageFeatureCommands', function () { assert.equal(b.commitCharacters, undefined); }); + test('vscode.executeCompletionItemProvider returns the wrong CompletionItemKinds in insiders #95715', async function () { + disposables.push(extHost.registerCompletionItemProvider(nullExtensionDescription, defaultSelector, { + provideCompletionItems(): any { + return [ + new types.CompletionItem('My Method', types.CompletionItemKind.Method), + new types.CompletionItem('My Property', types.CompletionItemKind.Property), + ]; + } + }, [])); + + await rpcProtocol.sync(); + + let list = await commands.executeCommand( + 'vscode.executeCompletionItemProvider', + model.uri, + new types.Position(0, 4), + undefined + ); + + assert.ok(list instanceof types.CompletionList); + assert.equal(list.items.length, 2); + + const [a, b] = list.items; + assert.equal(a.kind, types.CompletionItemKind.Method); + assert.equal(b.kind, types.CompletionItemKind.Property); + }); + // --- signatureHelp test('Parameter Hints, back and forth', async () => { diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts new file mode 100644 index 00000000000..c26fdb77f9d --- /dev/null +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -0,0 +1,437 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; +import { ExtHostNotebookDocument, ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { URI } from 'vs/base/common/uri'; +import { CellKind, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Position, Location, Range } from 'vs/workbench/api/common/extHostTypes'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import * as vscode from 'vscode'; +import { mock } from 'vs/workbench/test/common/workbenchTestServices'; +import { MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + + +suite('NotebookConcatDocument', function () { + + let rpcProtocol: TestRPCProtocol; + let notebook: ExtHostNotebookDocument; + let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; + let extHostDocuments: ExtHostDocuments; + let extHostNotebooks: ExtHostNotebookController; + const notebookUri = URI.parse('test:///notebook.file'); + const disposables = new DisposableStore(); + + setup(async function () { + disposables.clear(); + + rpcProtocol = new TestRPCProtocol(); + rpcProtocol.set(MainContext.MainThreadCommands, new class extends mock() { + $registerCommand() { } + }); + rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock() { + async $registerNotebookProvider() { } + async $unregisterNotebookProvider() { } + async $createNotebookDocument() { } + }); + extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); + extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors); + let reg = extHostNotebooks.registerNotebookProvider(nullExtensionDescription, 'test', new class extends mock() { + async resolveNotebook() { } + }); + await extHostNotebooks.$resolveNotebook('test', notebookUri); + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: 0, + changes: [[0, 0, [{ + handle: 0, + uri: CellUri.generate(notebookUri, 0), + source: ['### Heading'], + language: 'markdown', + cellKind: CellKind.Markdown, + outputs: [], + }]]] + }); + await extHostNotebooks.$updateActiveEditor('test', notebookUri); + + notebook = extHostNotebooks.activeNotebookDocument!; + + disposables.add(reg); + disposables.add(notebook); + disposables.add(extHostDocuments); + }); + + test('empty', function () { + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + assert.equal(doc.getText(), ''); + assert.equal(doc.version, 0); + + // assert.equal(doc.locationAt(new Position(0, 0)), undefined); + // assert.equal(doc.positionAt(SOME_FAKE_LOCATION?), undefined); + }); + + + function assertLocation(doc: vscode.NotebookConcatTextDocument, pos: Position, expected: Location, reverse = true) { + const actual = doc.locationAt(pos); + assert.equal(actual.uri.toString(), expected.uri.toString()); + assert.equal(actual.range.isEqual(expected.range), true); + + if (reverse) { + // reverse - offset + const offset = doc.offsetAt(pos); + assert.equal(doc.positionAt(offset).isEqual(pos), true); + + // reverse - pos + const actualPosition = doc.positionAt(actual); + assert.equal(actualPosition.isEqual(pos), true); + } + } + + function assertLines(doc: vscode.NotebookConcatTextDocument, ...lines: string[]) { + let actual = doc.getText().split(/\r\n|\n|\r/); + assert.deepStrictEqual(actual, lines); + } + + test('location, position mapping', function () { + + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[0, 0, [{ + handle: 1, + uri: CellUri.generate(notebook.uri, 1), + source: ['Hello', 'World', 'Hello World!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }, { + handle: 2, + uri: CellUri.generate(notebook.uri, 2), + source: ['Hallo', 'Welt', 'Hallo Welt!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + + + assert.equal(notebook.cells.length, 1 + 2); // markdown and code + + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); + + assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(5, 11), new Location(notebook.cells[1].uri, new Position(2, 11))); + assertLocation(doc, new Position(5, 12), new Location(notebook.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped + }); + + + test('location, position mapping, cell changes', function () { + + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + + // UPDATE 1 + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[0, 0, [{ + handle: 1, + uri: CellUri.generate(notebook.uri, 1), + source: ['Hello', 'World', 'Hello World!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + assert.equal(notebook.cells.length, 1 + 1); + assert.equal(doc.version, 1); + assertLines(doc, 'Hello', 'World', 'Hello World!'); + + assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(4, 0), new Location(notebook.cells[0].uri, new Position(2, 12)), false); // clamped + + + // UPDATE 2 + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[1, 0, [{ + handle: 2, + uri: CellUri.generate(notebook.uri, 2), + source: ['Hallo', 'Welt', 'Hallo Welt!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + + assert.equal(notebook.cells.length, 1 + 2); + assert.equal(doc.version, 2); + assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); + assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(5, 11), new Location(notebook.cells[1].uri, new Position(2, 11))); + assertLocation(doc, new Position(5, 12), new Location(notebook.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped + + // UPDATE 3 (remove cell #2 again) + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[1, 1, []]] + }); + assert.equal(notebook.cells.length, 1 + 1); + assert.equal(doc.version, 3); + assertLines(doc, 'Hello', 'World', 'Hello World!'); + assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(4, 0), new Location(notebook.cells[0].uri, new Position(2, 12)), false); // clamped + }); + + test('location, position mapping, cell-document changes', function () { + + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + + // UPDATE 1 + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[0, 0, [{ + handle: 1, + uri: CellUri.generate(notebook.uri, 1), + source: ['Hello', 'World', 'Hello World!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }, { + handle: 2, + uri: CellUri.generate(notebook.uri, 2), + source: ['Hallo', 'Welt', 'Hallo Welt!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + assert.equal(notebook.cells.length, 1 + 2); + assert.equal(doc.version, 1); + + assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); + assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(2, 12), new Location(notebook.cells[0].uri, new Position(2, 12))); + assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); + + // offset math + let cell1End = doc.offsetAt(new Position(2, 12)); + assert.equal(doc.positionAt(cell1End).isEqual(new Position(2, 12)), true); + + extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ + addedDocuments: [{ + uri: notebook.cells[0].uri, + versionId: 1, + lines: ['Hello', 'World', 'Hello World!'], + EOL: '\n', + modeId: '', + isDirty: false + }] + }); + + extHostDocuments.$acceptModelChanged(notebook.cells[0].uri, { + versionId: 0, + eol: '\n', + changes: [{ + range: { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 6 }, + rangeLength: 6, + rangeOffset: 12, + text: 'Hi' + }] + }, false); + assertLines(doc, 'Hello', 'World', 'Hi World!', 'Hallo', 'Welt', 'Hallo Welt!'); + assertLocation(doc, new Position(2, 12), new Location(notebook.cells[0].uri, new Position(2, 9)), false); + + assert.equal(doc.positionAt(cell1End).isEqual(new Position(3, 2)), true); + + }); + + test('selector', function () { + + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[0, 0, [{ + handle: 1, + uri: CellUri.generate(notebook.uri, 1), + source: ['fooLang-document'], + language: 'fooLang', + cellKind: CellKind.Code, + outputs: [], + }, { + handle: 2, + uri: CellUri.generate(notebook.uri, 2), + source: ['barLang-document'], + language: 'barLang', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + + const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, 'fooLang'); + const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, 'barLang'); + + assertLines(mixedDoc, 'fooLang-document', 'barLang-document'); + assertLines(fooLangDoc, 'fooLang-document'); + assertLines(barLangDoc, 'barLang-document'); + + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[2, 0, [{ + handle: 3, + uri: CellUri.generate(notebook.uri, 3), + source: ['barLang-document2'], + language: 'barLang', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + + assertLines(mixedDoc, 'fooLang-document', 'barLang-document', 'barLang-document2'); + assertLines(fooLangDoc, 'fooLang-document'); + assertLines(barLangDoc, 'barLang-document', 'barLang-document2'); + }); + + function assertOffsetAtPosition(doc: vscode.NotebookConcatTextDocument, offset: number, expected: { line: number, character: number }, reverse = true) { + const actual = doc.positionAt(offset); + + assert.equal(actual.line, expected.line); + assert.equal(actual.character, expected.character); + + if (reverse) { + const actualOffset = doc.offsetAt(actual); + assert.equal(actualOffset, offset); + } + } + + + test('offsetAt(position) <-> positionAt(offset)', function () { + + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[0, 0, [{ + handle: 1, + uri: CellUri.generate(notebook.uri, 1), + source: ['Hello', 'World', 'Hello World!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }, { + handle: 2, + uri: CellUri.generate(notebook.uri, 2), + source: ['Hallo', 'Welt', 'Hallo Welt!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + + assert.equal(notebook.cells.length, 1 + 2); // markdown and code + + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); + + assertOffsetAtPosition(doc, 0, { line: 0, character: 0 }); + assertOffsetAtPosition(doc, 1, { line: 0, character: 1 }); + assertOffsetAtPosition(doc, 9, { line: 1, character: 3 }); + assertOffsetAtPosition(doc, 32, { line: 4, character: 1 }); + assertOffsetAtPosition(doc, 47, { line: 5, character: 11 }); + }); + + + function assertLocationAtPosition(doc: vscode.NotebookConcatTextDocument, pos: { line: number, character: number }, expected: { uri: URI, line: number, character: number }, reverse = true) { + + const actual = doc.locationAt(new Position(pos.line, pos.character)); + assert.equal(actual.uri.toString(), expected.uri.toString()); + assert.equal(actual.range.start.line, expected.line); + assert.equal(actual.range.end.line, expected.line); + assert.equal(actual.range.start.character, expected.character); + assert.equal(actual.range.end.character, expected.character); + + if (reverse) { + const actualPos = doc.positionAt(actual); + assert.equal(actualPos.line, pos.line); + assert.equal(actualPos.character, pos.character); + } + } + + test('locationAt(position) <-> positionAt(location)', function () { + + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[0, 0, [{ + handle: 1, + uri: CellUri.generate(notebook.uri, 1), + source: ['Hello', 'World', 'Hello World!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }, { + handle: 2, + uri: CellUri.generate(notebook.uri, 2), + source: ['Hallo', 'Welt', 'Hallo Welt!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + + assert.equal(notebook.cells.length, 1 + 2); // markdown and code + + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); + + assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.cells[0].uri, line: 0, character: 0 }); + assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.cells[0].uri, line: 2, character: 0 }); + assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.cells[0].uri, line: 2, character: 12 }); + assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.cells[1].uri, line: 0, character: 0 }); + assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.cells[1].uri, line: 2, character: 0 }); + assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.cells[1].uri, line: 2, character: 11 }); + }); + + test('getText(range)', function () { + + extHostNotebooks.$acceptModelChanged(notebookUri, { + versionId: notebook.versionId + 1, + changes: [[0, 0, [{ + handle: 1, + uri: CellUri.generate(notebook.uri, 1), + source: ['Hello', 'World', 'Hello World!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }, { + handle: 2, + uri: CellUri.generate(notebook.uri, 2), + source: ['Hallo', 'Welt', 'Hallo Welt!'], + language: 'test', + cellKind: CellKind.Code, + outputs: [], + }]]] + }); + + assert.equal(notebook.cells.length, 1 + 2); // markdown and code + + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); + + assert.equal(doc.getText(new Range(0, 0, 0, 0)), ''); + assert.equal(doc.getText(new Range(0, 0, 1, 0)), 'Hello\n'); + assert.equal(doc.getText(new Range(2, 0, 4, 0)), 'Hello World!\nHallo\n'); + }); +}); diff --git a/src/vs/workbench/test/browser/parts/views/views.test.ts b/src/vs/workbench/test/browser/parts/views/views.test.ts deleted file mode 100644 index 066b67fa550..00000000000 --- a/src/vs/workbench/test/browser/parts/views/views.test.ts +++ /dev/null @@ -1,628 +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 { ContributableViewsModel, IViewState } from 'vs/workbench/browser/parts/views/views'; -import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewDescriptorService, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { move } from 'vs/base/common/arrays'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; -import sinon = require('sinon'); -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { ViewDescriptorService } from 'vs/workbench/services/views/browser/viewDescriptorService'; -import { assertIsDefined } from 'vs/base/common/types'; - -const container = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); -const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); - -class ViewDescriptorSequence { - - readonly elements: IViewDescriptor[]; - private disposables: IDisposable[] = []; - - constructor(model: ContributableViewsModel) { - this.elements = [...model.visibleViewDescriptors]; - model.onDidAdd(added => added.forEach(({ viewDescriptor, index }) => this.elements.splice(index, 0, viewDescriptor)), null, this.disposables); - model.onDidRemove(removed => removed.sort((a, b) => b.index - a.index).forEach(({ index }) => this.elements.splice(index, 1)), null, this.disposables); - model.onDidMove(({ from, to }) => move(this.elements, from.index, to.index), null, this.disposables); - } - - dispose() { - this.disposables = dispose(this.disposables); - } -} - -suite('ContributableViewsModel', () => { - - let viewDescriptorService: IViewDescriptorService; - let contextKeyService: IContextKeyService; - - setup(() => { - const instantiationService: TestInstantiationService = workbenchInstantiationService(); - contextKeyService = instantiationService.createInstance(ContextKeyService); - instantiationService.stub(IContextKeyService, contextKeyService); - viewDescriptorService = instantiationService.createInstance(ViewDescriptorService); - }); - - teardown(() => { - ViewsRegistry.deregisterViews(ViewsRegistry.getViews(container), container); - }); - - test('empty model', function () { - const model = new ContributableViewsModel(container, viewDescriptorService); - assert.equal(model.visibleViewDescriptors.length, 0); - }); - - test('register/unregister', () => { - const model = new ContributableViewsModel(container, viewDescriptorService); - const seq = new ViewDescriptorSequence(model); - - assert.equal(model.visibleViewDescriptors.length, 0); - assert.equal(seq.elements.length, 0); - - const viewDescriptor: IViewDescriptor = { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1' - }; - - ViewsRegistry.registerViews([viewDescriptor], container); - - assert.equal(model.visibleViewDescriptors.length, 1); - assert.equal(seq.elements.length, 1); - assert.deepEqual(model.visibleViewDescriptors[0], viewDescriptor); - assert.deepEqual(seq.elements[0], viewDescriptor); - - ViewsRegistry.deregisterViews([viewDescriptor], container); - - assert.equal(model.visibleViewDescriptors.length, 0); - assert.equal(seq.elements.length, 0); - }); - - test('when contexts', async function () { - const model = new ContributableViewsModel(container, viewDescriptorService); - const seq = new ViewDescriptorSequence(model); - - assert.equal(model.visibleViewDescriptors.length, 0); - assert.equal(seq.elements.length, 0); - - const viewDescriptor: IViewDescriptor = { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1', - when: ContextKeyExpr.equals('showview1', true) - }; - - ViewsRegistry.registerViews([viewDescriptor], container); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should not appear since context isnt in'); - assert.equal(seq.elements.length, 0); - - const key = contextKeyService.createKey('showview1', false); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should still not appear since showview1 isnt true'); - assert.equal(seq.elements.length, 0); - - key.set(true); - await new Promise(c => setTimeout(c, 30)); - assert.equal(model.visibleViewDescriptors.length, 1, 'view should appear'); - assert.equal(seq.elements.length, 1); - assert.deepEqual(model.visibleViewDescriptors[0], viewDescriptor); - assert.equal(seq.elements[0], viewDescriptor); - - key.set(false); - await new Promise(c => setTimeout(c, 30)); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should disappear'); - assert.equal(seq.elements.length, 0); - - ViewsRegistry.deregisterViews([viewDescriptor], container); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should not be there anymore'); - assert.equal(seq.elements.length, 0); - - key.set(true); - await new Promise(c => setTimeout(c, 30)); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should not be there anymore'); - assert.equal(seq.elements.length, 0); - }); - - test('when contexts - multiple', async function () { - const model = new ContributableViewsModel(container, viewDescriptorService); - const seq = new ViewDescriptorSequence(model); - - const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1' }; - const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2', when: ContextKeyExpr.equals('showview2', true) }; - - ViewsRegistry.registerViews([view1, view2], container); - assert.deepEqual(model.visibleViewDescriptors, [view1], 'only view1 should be visible'); - assert.deepEqual(seq.elements, [view1], 'only view1 should be visible'); - - const key = contextKeyService.createKey('showview2', false); - assert.deepEqual(model.visibleViewDescriptors, [view1], 'still only view1 should be visible'); - assert.deepEqual(seq.elements, [view1], 'still only view1 should be visible'); - - key.set(true); - await new Promise(c => setTimeout(c, 30)); - assert.deepEqual(model.visibleViewDescriptors, [view1, view2], 'both views should be visible'); - assert.deepEqual(seq.elements, [view1, view2], 'both views should be visible'); - - ViewsRegistry.deregisterViews([view1, view2], container); - }); - - test('when contexts - multiple 2', async function () { - const model = new ContributableViewsModel(container, viewDescriptorService); - const seq = new ViewDescriptorSequence(model); - - const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1', when: ContextKeyExpr.equals('showview1', true) }; - const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2' }; - - ViewsRegistry.registerViews([view1, view2], container); - assert.deepEqual(model.visibleViewDescriptors, [view2], 'only view2 should be visible'); - assert.deepEqual(seq.elements, [view2], 'only view2 should be visible'); - - const key = contextKeyService.createKey('showview1', false); - assert.deepEqual(model.visibleViewDescriptors, [view2], 'still only view2 should be visible'); - assert.deepEqual(seq.elements, [view2], 'still only view2 should be visible'); - - key.set(true); - await new Promise(c => setTimeout(c, 30)); - assert.deepEqual(model.visibleViewDescriptors, [view1, view2], 'both views should be visible'); - assert.deepEqual(seq.elements, [view1, view2], 'both views should be visible'); - - ViewsRegistry.deregisterViews([view1, view2], container); - }); - - test('setVisible', () => { - const model = new ContributableViewsModel(container, viewDescriptorService); - const seq = new ViewDescriptorSequence(model); - - const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1', canToggleVisibility: true }; - const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2', canToggleVisibility: true }; - const view3: IViewDescriptor = { id: 'view3', ctorDescriptor: null!, name: 'Test View 3', canToggleVisibility: true }; - - ViewsRegistry.registerViews([view1, view2, view3], container); - assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3]); - assert.deepEqual(seq.elements, [view1, view2, view3]); - - model.setVisible('view2', true); - assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3], 'nothing should happen'); - assert.deepEqual(seq.elements, [view1, view2, view3]); - - model.setVisible('view2', false); - assert.deepEqual(model.visibleViewDescriptors, [view1, view3], 'view2 should hide'); - assert.deepEqual(seq.elements, [view1, view3]); - - model.setVisible('view1', false); - assert.deepEqual(model.visibleViewDescriptors, [view3], 'view1 should hide'); - assert.deepEqual(seq.elements, [view3]); - - model.setVisible('view3', false); - assert.deepEqual(model.visibleViewDescriptors, [], 'view3 shoud hide'); - assert.deepEqual(seq.elements, []); - - model.setVisible('view1', true); - assert.deepEqual(model.visibleViewDescriptors, [view1], 'view1 should show'); - assert.deepEqual(seq.elements, [view1]); - - model.setVisible('view3', true); - assert.deepEqual(model.visibleViewDescriptors, [view1, view3], 'view3 should show'); - assert.deepEqual(seq.elements, [view1, view3]); - - model.setVisible('view2', true); - assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3], 'view2 should show'); - assert.deepEqual(seq.elements, [view1, view2, view3]); - - ViewsRegistry.deregisterViews([view1, view2, view3], container); - assert.deepEqual(model.visibleViewDescriptors, []); - assert.deepEqual(seq.elements, []); - }); - - test('move', () => { - const model = new ContributableViewsModel(container, viewDescriptorService); - const seq = new ViewDescriptorSequence(model); - - const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1' }; - const view2: IViewDescriptor = { id: 'view2', ctorDescriptor: null!, name: 'Test View 2' }; - const view3: IViewDescriptor = { id: 'view3', ctorDescriptor: null!, name: 'Test View 3' }; - - ViewsRegistry.registerViews([view1, view2, view3], container); - assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3], 'model views should be OK'); - assert.deepEqual(seq.elements, [view1, view2, view3], 'sql views should be OK'); - - model.move('view3', 'view1'); - assert.deepEqual(model.visibleViewDescriptors, [view3, view1, view2], 'view3 should go to the front'); - assert.deepEqual(seq.elements, [view3, view1, view2]); - - model.move('view1', 'view2'); - assert.deepEqual(model.visibleViewDescriptors, [view3, view2, view1], 'view1 should go to the end'); - assert.deepEqual(seq.elements, [view3, view2, view1]); - - model.move('view1', 'view3'); - assert.deepEqual(model.visibleViewDescriptors, [view1, view3, view2], 'view1 should go to the front'); - assert.deepEqual(seq.elements, [view1, view3, view2]); - - model.move('view2', 'view3'); - assert.deepEqual(model.visibleViewDescriptors, [view1, view2, view3], 'view2 should go to the middle'); - assert.deepEqual(seq.elements, [view1, view2, view3]); - }); - - test('view states', async function () { - const viewStates = new Map(); - viewStates.set('view1', { visibleGlobal: false, collapsed: false, visibleWorkspace: undefined }); - const model = new ContributableViewsModel(container, viewDescriptorService, viewStates); - const seq = new ViewDescriptorSequence(model); - - assert.equal(model.visibleViewDescriptors.length, 0); - assert.equal(seq.elements.length, 0); - - const viewDescriptor: IViewDescriptor = { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1' - }; - - ViewsRegistry.registerViews([viewDescriptor], container); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should not appear since it was set not visible in view state'); - assert.equal(seq.elements.length, 0); - }); - - test('view states and when contexts', async function () { - const viewStates = new Map(); - viewStates.set('view1', { visibleGlobal: false, collapsed: false, visibleWorkspace: undefined }); - const model = new ContributableViewsModel(container, viewDescriptorService, viewStates); - const seq = new ViewDescriptorSequence(model); - - assert.equal(model.visibleViewDescriptors.length, 0); - assert.equal(seq.elements.length, 0); - - const viewDescriptor: IViewDescriptor = { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1', - when: ContextKeyExpr.equals('showview1', true) - }; - - ViewsRegistry.registerViews([viewDescriptor], container); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should not appear since context isnt in'); - assert.equal(seq.elements.length, 0); - - const key = contextKeyService.createKey('showview1', false); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should still not appear since showview1 isnt true'); - assert.equal(seq.elements.length, 0); - - key.set(true); - await new Promise(c => setTimeout(c, 30)); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should still not appear since it was set not visible in view state'); - assert.equal(seq.elements.length, 0); - }); - - test('view states and when contexts multiple views', async function () { - const viewStates = new Map(); - viewStates.set('view1', { visibleGlobal: false, collapsed: false, visibleWorkspace: undefined }); - const model = new ContributableViewsModel(container, viewDescriptorService, viewStates); - const seq = new ViewDescriptorSequence(model); - - assert.equal(model.visibleViewDescriptors.length, 0); - assert.equal(seq.elements.length, 0); - - const view1: IViewDescriptor = { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1', - when: ContextKeyExpr.equals('showview', true) - }; - const view2: IViewDescriptor = { - id: 'view2', - ctorDescriptor: null!, - name: 'Test View 2', - }; - const view3: IViewDescriptor = { - id: 'view3', - ctorDescriptor: null!, - name: 'Test View 3', - when: ContextKeyExpr.equals('showview', true) - }; - - ViewsRegistry.registerViews([view1, view2, view3], container); - assert.deepEqual(model.visibleViewDescriptors, [view2], 'Only view2 should be visible'); - assert.deepEqual(seq.elements, [view2]); - - const key = contextKeyService.createKey('showview', false); - assert.deepEqual(model.visibleViewDescriptors, [view2], 'Only view2 should be visible'); - assert.deepEqual(seq.elements, [view2]); - - key.set(true); - await new Promise(c => setTimeout(c, 30)); - assert.deepEqual(model.visibleViewDescriptors, [view2, view3], 'view3 should be visible'); - assert.deepEqual(seq.elements, [view2, view3]); - - key.set(false); - await new Promise(c => setTimeout(c, 30)); - assert.deepEqual(model.visibleViewDescriptors, [view2], 'Only view2 should be visible'); - assert.deepEqual(seq.elements, [view2]); - }); - - test('remove event is not triggered if view was hidden and removed', async function () { - const model = new ContributableViewsModel(container, viewDescriptorService); - const seq = new ViewDescriptorSequence(model); - - const viewDescriptor: IViewDescriptor = { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1', - when: ContextKeyExpr.equals('showview1', true), - canToggleVisibility: true - }; - - ViewsRegistry.registerViews([viewDescriptor], container); - - const key = contextKeyService.createKey('showview1', true); - await new Promise(c => setTimeout(c, 30)); - assert.equal(model.visibleViewDescriptors.length, 1, 'view should appear after context is set'); - assert.equal(seq.elements.length, 1); - - model.setVisible('view1', false); - assert.equal(model.visibleViewDescriptors.length, 0, 'view should disappear after setting visibility to false'); - assert.equal(seq.elements.length, 0); - - const target = sinon.spy(model.onDidRemove); - key.set(false); - await new Promise(c => setTimeout(c, 30)); - assert.ok(!target.called, 'remove event should not be called since it is already hidden'); - }); - -}); - -const sidebarContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: 'testSidebar', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); -const panelContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: 'testPanel', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Panel); - -suite('ViewDescriptorService', () => { - - let viewDescriptorService: IViewDescriptorService; - - setup(() => { - const instantiationService: TestInstantiationService = workbenchInstantiationService(); - viewDescriptorService = instantiationService.createInstance(ViewDescriptorService); - }); - - teardown(() => { - ViewsRegistry.deregisterViews(ViewsRegistry.getViews(sidebarContainer), sidebarContainer); - ViewsRegistry.deregisterViews(ViewsRegistry.getViews(panelContainer), panelContainer); - }); - - test('Empty Containers', function () { - const sidebarViews = viewDescriptorService.getViewDescriptors(sidebarContainer); - const panelViews = viewDescriptorService.getViewDescriptors(panelContainer); - assert.equal(sidebarViews.allViewDescriptors.length, 0, 'The sidebar container should have no views yet.'); - assert.equal(panelViews.allViewDescriptors.length, 0, 'The panel container should have no views yet.'); - }); - - test('Register/Deregister', () => { - const viewDescriptors: IViewDescriptor[] = [ - { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1', - canMoveView: true - }, - { - id: 'view2', - ctorDescriptor: null!, - name: 'Test View 2', - canMoveView: true - }, - { - id: 'view3', - ctorDescriptor: null!, - name: 'Test View 3', - canMoveView: true - } - ]; - - - ViewsRegistry.registerViews(viewDescriptors.slice(0, 2), sidebarContainer); - ViewsRegistry.registerViews(viewDescriptors.slice(2), panelContainer); - - - let sidebarViews = viewDescriptorService.getViewDescriptors(sidebarContainer); - let panelViews = viewDescriptorService.getViewDescriptors(panelContainer); - - assert.equal(sidebarViews.activeViewDescriptors.length, 2, 'Sidebar should have 2 views'); - assert.equal(panelViews.activeViewDescriptors.length, 1, 'Panel should have 1 view'); - - ViewsRegistry.deregisterViews(viewDescriptors.slice(0, 2), sidebarContainer); - ViewsRegistry.deregisterViews(viewDescriptors.slice(2), panelContainer); - - - sidebarViews = viewDescriptorService.getViewDescriptors(sidebarContainer); - panelViews = viewDescriptorService.getViewDescriptors(panelContainer); - - assert.equal(sidebarViews.activeViewDescriptors.length, 0, 'Sidebar should have no views'); - assert.equal(panelViews.activeViewDescriptors.length, 0, 'Panel should have no views'); - }); - - test('move views to existing containers', async function () { - const viewDescriptors: IViewDescriptor[] = [ - { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1', - canMoveView: true - }, - { - id: 'view2', - ctorDescriptor: null!, - name: 'Test View 2', - canMoveView: true - }, - { - id: 'view3', - ctorDescriptor: null!, - name: 'Test View 3', - canMoveView: true - } - ]; - - ViewsRegistry.registerViews(viewDescriptors.slice(0, 2), sidebarContainer); - ViewsRegistry.registerViews(viewDescriptors.slice(2), panelContainer); - - viewDescriptorService.moveViewsToContainer(viewDescriptors.slice(2), sidebarContainer); - viewDescriptorService.moveViewsToContainer(viewDescriptors.slice(0, 2), panelContainer); - - let sidebarViews = viewDescriptorService.getViewDescriptors(sidebarContainer); - let panelViews = viewDescriptorService.getViewDescriptors(panelContainer); - - assert.equal(sidebarViews.activeViewDescriptors.length, 1, 'Sidebar should have 2 views'); - assert.equal(panelViews.activeViewDescriptors.length, 2, 'Panel should have 1 view'); - - assert.notEqual(sidebarViews.activeViewDescriptors.indexOf(viewDescriptors[2]), -1, `Sidebar should have ${viewDescriptors[2].name}`); - assert.notEqual(panelViews.activeViewDescriptors.indexOf(viewDescriptors[0]), -1, `Panel should have ${viewDescriptors[0].name}`); - assert.notEqual(panelViews.activeViewDescriptors.indexOf(viewDescriptors[1]), -1, `Panel should have ${viewDescriptors[1].name}`); - }); - - test('move views to generated containers', async function () { - const viewDescriptors: IViewDescriptor[] = [ - { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1', - canMoveView: true - }, - { - id: 'view2', - ctorDescriptor: null!, - name: 'Test View 2', - canMoveView: true - }, - { - id: 'view3', - ctorDescriptor: null!, - name: 'Test View 3', - canMoveView: true - } - ]; - - ViewsRegistry.registerViews(viewDescriptors.slice(0, 2), sidebarContainer); - ViewsRegistry.registerViews(viewDescriptors.slice(2), panelContainer); - - viewDescriptorService.moveViewToLocation(viewDescriptors[0], ViewContainerLocation.Panel); - viewDescriptorService.moveViewToLocation(viewDescriptors[2], ViewContainerLocation.Sidebar); - - let sidebarViews = viewDescriptorService.getViewDescriptors(sidebarContainer); - let panelViews = viewDescriptorService.getViewDescriptors(panelContainer); - - assert.equal(sidebarViews.activeViewDescriptors.length, 1, 'Sidebar container should have 1 view'); - assert.equal(panelViews.activeViewDescriptors.length, 0, 'Panel container should have no views'); - - const generatedPanel = assertIsDefined(viewDescriptorService.getViewContainer(viewDescriptors[0].id)); - const generatedSidebar = assertIsDefined(viewDescriptorService.getViewContainer(viewDescriptors[2].id)); - - assert.equal(viewDescriptorService.getViewContainerLocation(generatedPanel), ViewContainerLocation.Panel, 'Generated Panel should be in located in the panel'); - assert.equal(viewDescriptorService.getViewContainerLocation(generatedSidebar), ViewContainerLocation.Sidebar, 'Generated Sidebar should be in located in the sidebar'); - - assert.equal(viewDescriptorService.getViewContainerLocation(generatedPanel), viewDescriptorService.getViewLocation(viewDescriptors[0].id), 'Panel view location and container location should match'); - assert.equal(viewDescriptorService.getViewContainerLocation(generatedSidebar), viewDescriptorService.getViewLocation(viewDescriptors[2].id), 'Sidebar view location and container location should match'); - - assert.equal(viewDescriptorService.getDefaultContainer(viewDescriptors[2].id), panelContainer, `${viewDescriptors[2].name} has wrong default container`); - assert.equal(viewDescriptorService.getDefaultContainer(viewDescriptors[0].id), sidebarContainer, `${viewDescriptors[0].name} has wrong default container`); - - viewDescriptorService.moveViewToLocation(viewDescriptors[0], ViewContainerLocation.Sidebar); - viewDescriptorService.moveViewToLocation(viewDescriptors[2], ViewContainerLocation.Panel); - - sidebarViews = viewDescriptorService.getViewDescriptors(sidebarContainer); - panelViews = viewDescriptorService.getViewDescriptors(panelContainer); - - assert.equal(sidebarViews.activeViewDescriptors.length, 1, 'Sidebar should have 2 views'); - assert.equal(panelViews.activeViewDescriptors.length, 0, 'Panel should have 1 view'); - - assert.equal(viewDescriptorService.getViewLocation(viewDescriptors[0].id), ViewContainerLocation.Sidebar, 'View should be located in the sidebar'); - assert.equal(viewDescriptorService.getViewLocation(viewDescriptors[2].id), ViewContainerLocation.Panel, 'View should be located in the panel'); - }); - - test('move view events', async function () { - const viewDescriptors: IViewDescriptor[] = [ - { - id: 'view1', - ctorDescriptor: null!, - name: 'Test View 1', - canMoveView: true - }, - { - id: 'view2', - ctorDescriptor: null!, - name: 'Test View 2', - canMoveView: true - }, - { - id: 'view3', - ctorDescriptor: null!, - name: 'Test View 3', - canMoveView: true - } - ]; - - - let expectedSequence = ''; - let actualSequence = ''; - const disposables = []; - - const containerMoveString = (view: IViewDescriptor, from: ViewContainer, to: ViewContainer) => { - return `Moved ${view.id} from ${from.id} to ${to.id}\n`; - }; - - const locationMoveString = (view: IViewDescriptor, from: ViewContainerLocation, to: ViewContainerLocation) => { - return `Moved ${view.id} from ${from === ViewContainerLocation.Sidebar ? 'Sidebar' : 'Panel'} to ${to === ViewContainerLocation.Sidebar ? 'Sidebar' : 'Panel'}\n`; - }; - disposables.push(viewDescriptorService.onDidChangeContainer(({ views, from, to }) => { - views.forEach(view => { - actualSequence += containerMoveString(view, from, to); - }); - })); - - disposables.push(viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { - views.forEach(view => { - actualSequence += locationMoveString(view, from, to); - }); - })); - - ViewsRegistry.registerViews(viewDescriptors.slice(0, 2), sidebarContainer); - ViewsRegistry.registerViews(viewDescriptors.slice(2), panelContainer); - - expectedSequence += locationMoveString(viewDescriptors[0], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); - viewDescriptorService.moveViewToLocation(viewDescriptors[0], ViewContainerLocation.Panel); - expectedSequence += containerMoveString(viewDescriptors[0], sidebarContainer, viewDescriptorService.getViewContainer(viewDescriptors[0].id)!); - - expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Panel, ViewContainerLocation.Sidebar); - viewDescriptorService.moveViewToLocation(viewDescriptors[2], ViewContainerLocation.Sidebar); - expectedSequence += containerMoveString(viewDescriptors[2], panelContainer, viewDescriptorService.getViewContainer(viewDescriptors[2].id)!); - - - expectedSequence += locationMoveString(viewDescriptors[0], ViewContainerLocation.Panel, ViewContainerLocation.Sidebar); - expectedSequence += containerMoveString(viewDescriptors[0], viewDescriptorService.getViewContainer(viewDescriptors[0].id)!, sidebarContainer); - viewDescriptorService.moveViewsToContainer([viewDescriptors[0]], sidebarContainer); - - expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); - expectedSequence += containerMoveString(viewDescriptors[2], viewDescriptorService.getViewContainer(viewDescriptors[2].id)!, panelContainer); - viewDescriptorService.moveViewsToContainer([viewDescriptors[2]], panelContainer); - - expectedSequence += locationMoveString(viewDescriptors[0], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); - expectedSequence += containerMoveString(viewDescriptors[0], sidebarContainer, panelContainer); - viewDescriptorService.moveViewsToContainer([viewDescriptors[0]], panelContainer); - - expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Panel, ViewContainerLocation.Sidebar); - expectedSequence += containerMoveString(viewDescriptors[2], panelContainer, sidebarContainer); - viewDescriptorService.moveViewsToContainer([viewDescriptors[2]], sidebarContainer); - - expectedSequence += locationMoveString(viewDescriptors[1], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); - expectedSequence += locationMoveString(viewDescriptors[2], ViewContainerLocation.Sidebar, ViewContainerLocation.Panel); - expectedSequence += containerMoveString(viewDescriptors[1], sidebarContainer, panelContainer); - expectedSequence += containerMoveString(viewDescriptors[2], sidebarContainer, panelContainer); - viewDescriptorService.moveViewsToContainer([viewDescriptors[1], viewDescriptors[2]], panelContainer); - - assert.equal(actualSequence, expectedSequence, 'Event sequence not matching expected sequence'); - }); - -}); diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 9c76a188fb7..11647ab25e9 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -105,6 +105,52 @@ interface IHomeIndicator { title: string; } +interface IDefaultSideBarLayout { + visible?: boolean; + containers?: ({ + id: 'explorer' | 'run' | 'scm' | 'search' | 'extensions' | 'remote' | string; + active: true; + order?: number; + views?: { + id: string; + order?: number; + visible?: boolean; + collapsed?: boolean; + }[]; + } | { + id: 'explorer' | 'run' | 'scm' | 'search' | 'extensions' | 'remote' | string; + active?: false | undefined; + order?: number; + visible?: boolean; + views?: { + id: string; + order?: number; + visible?: boolean; + collapsed?: boolean; + }[]; + })[]; +} + +interface IDefaultPanelLayout { + visible?: boolean; + containers?: ({ + id: 'terminal' | 'debug' | 'problems' | 'output' | 'comments' | string; + order?: number; + active: true; + } | { + id: 'terminal' | 'debug' | 'problems' | 'output' | 'comments' | string; + order?: number; + active?: false | undefined; + visible?: boolean; + })[]; +} + +interface IDefaultLayout { + sidebar?: IDefaultSideBarLayout; + panel?: IDefaultPanelLayout; + // editors?: IDefaultWorkspaceEditorsLayout +} + interface IWorkbenchConstructionOptions { //#region Connection related configuration @@ -221,6 +267,8 @@ interface IWorkbenchConstructionOptions { readonly driver?: boolean; //#endregion + + defaultLayout?: IDefaultLayout; } interface IWorkbench { @@ -351,7 +399,12 @@ export { commands, // Home Indicator - IHomeIndicator + IHomeIndicator, + + // Default layout + IDefaultLayout, + IDefaultPanelLayout, + IDefaultSideBarLayout, }; //#endregion diff --git a/yarn.lock b/yarn.lock index 54462bc339e..b6dd7b4c8f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9265,10 +9265,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^3.9.0-dev.20200413: - version "3.9.0-dev.20200413" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200413.tgz#dd38331435f5c30867f19c94fe7cfc567ec13e20" - integrity sha512-wpPljcRc3uLTVpQ8mtKTkS42ipVnW1E7lU5iII+JSKaUDH1ymeo3duvMz8o2sCRUDoZR0SE0SLcinaZ1q4gD7A== +typescript@^3.9.0-dev.20200420: + version "3.9.0-dev.20200420" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200420.tgz#99c2bc0936dbf4479b0b5260d80475ed494b1532" + integrity sha512-36MW6V+oXNnsSgliSjUWvtOkO21g9+iFGHPFv+O06HsCl3dcuqzBac17m8xuOuWo1LUlEgS6yAnD9fiVgvmCfg== uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3"