diff options
author | Johannes Rieken <johannes.rieken@gmail.com> | 2022-04-04 11:13:25 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-04 11:13:25 +0300 |
commit | 4ef3ed3ce8d7ab1857d41454449d32f946d3ac8c (patch) | |
tree | f211e51646c00aa397afdb0f866b9a2aae4f17df /src/vs/workbench/contrib | |
parent | d8eee16887698292885ea5f7bc4dac397d0377fb (diff) | |
parent | 2ada17080cd3e351e093e5fce40d36f16db3124a (diff) |
Merge branch 'main' into joh/cellUri
Diffstat (limited to 'src/vs/workbench/contrib')
141 files changed, 1840 insertions, 1081 deletions
diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts index 6b0bb5fdf18..602b81639e2 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -37,8 +37,8 @@ export class BulkCellEdits { @INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService, ) { } - async apply(): Promise<void> { - + async apply(): Promise<readonly URI[]> { + const resources: URI[] = []; const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); for (let group of editsByNotebook) { @@ -56,10 +56,14 @@ export class BulkCellEdits { // apply edits const edits = group.map(entry => entry.cellEdit); - ref.object.notebook.applyEdits(edits, true, undefined, () => undefined, this._undoRedoGroup); + ref.object.notebook.applyEdits(edits, true, undefined, () => undefined, this._undoRedoGroup, true); ref.dispose(); this._progress.report(undefined); + + resources.push(first.resource); } + + return resources; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index a259de1b6d5..aaab23b4da9 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -21,7 +21,12 @@ import { LinkedList } from 'vs/base/common/linkedList'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ResourceMap } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } from 'vs/base/common/map'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { URI } from 'vs/base/common/uri'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; class BulkEdit { @@ -67,10 +72,10 @@ class BulkEdit { } } - async perform(): Promise<void> { + async perform(): Promise<readonly URI[]> { if (this._edits.length === 0) { - return; + return []; } const ranges: number[] = [1]; @@ -88,6 +93,7 @@ class BulkEdit { // Increment by percentage points since progress API expects that const progress: IProgress<void> = { report: _ => this._progress.report({ increment: 100 / this._edits.length }) }; + const resources: (readonly URI[])[] = []; let index = 0; for (let range of ranges) { if (this._token.isCancellationRequested) { @@ -95,34 +101,36 @@ class BulkEdit { } const group = this._edits.slice(index, index + range); if (group[0] instanceof ResourceFileEdit) { - await this._performFileEdits(<ResourceFileEdit[]>group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress); + resources.push(await this._performFileEdits(<ResourceFileEdit[]>group, this._undoRedoGroup, this._undoRedoSource, this._confirmBeforeUndo, progress)); } else if (group[0] instanceof ResourceTextEdit) { - await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress); + resources.push(await this._performTextEdits(<ResourceTextEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress)); } else if (group[0] instanceof ResourceNotebookCellEdit) { - await this._performCellEdits(<ResourceNotebookCellEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress); + resources.push(await this._performCellEdits(<ResourceNotebookCellEdit[]>group, this._undoRedoGroup, this._undoRedoSource, progress)); } else { console.log('UNKNOWN EDIT'); } index = index + range; } + + return resources.flat(); } - private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress<void>) { + private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress<void>): Promise<readonly URI[]> { this._logService.debug('_performFileEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits); - await model.apply(); + return await model.apply(); } - private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<void> { + private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> { this._logService.debug('_performTextEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', this._editor, undoRedoGroup, undoRedoSource, progress, this._token, edits); - await model.apply(); + return await model.apply(); } - private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<void> { + private async _performCellEdits(edits: ResourceNotebookCellEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress<void>): Promise<readonly URI[]> { this._logService.debug('_performCellEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkCellEdits, undoRedoGroup, undoRedoSource, progress, this._token, edits); - await model.apply(); + return await model.apply(); } } @@ -138,7 +146,9 @@ export class BulkEditService implements IBulkEditService { @ILogService private readonly _logService: ILogService, @IEditorService private readonly _editorService: IEditorService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, - @IDialogService private readonly _dialogService: IDialogService + @IDialogService private readonly _dialogService: IDialogService, + @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, + @IConfigurationService private readonly _configService: IConfigurationService, ) { } setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable { @@ -212,8 +222,15 @@ export class BulkEditService implements IBulkEditService { let listener: IDisposable | undefined; try { - listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this.shouldVeto(label, e.reason), 'veto.blukEditService')); - await bulkEdit.perform(); + listener = this._lifecycleService.onBeforeShutdown(e => e.veto(this._shouldVeto(label, e.reason), 'veto.blukEditService')); + const resources = await bulkEdit.perform(); + + // when enabled (option AND setting) loop over all dirty working copies and trigger save + // for those that were involved in this bulk edit operation. + if (options?.respectAutoSaveConfig && this._configService.getValue(autoSaveSetting) === true && resources.length > 1) { + await this._saveAll(resources); + } + return { ariaSummary: bulkEdit.ariaMessage() }; } catch (err) { // console.log('apply FAILED'); @@ -226,7 +243,23 @@ export class BulkEditService implements IBulkEditService { } } - private async shouldVeto(label: string | undefined, reason: ShutdownReason): Promise<boolean> { + private async _saveAll(resources: readonly URI[]) { + const set = new ResourceSet(resources); + const saves = this._workingCopyService.dirtyWorkingCopies.map(async (copy) => { + if (set.has(copy.resource)) { + await copy.save(); + } + }); + + const result = await Promise.allSettled(saves); + for (const item of result) { + if (item.status === 'rejected') { + this._logService.warn(item.reason); + } + } + } + + private async _shouldVeto(label: string | undefined, reason: ShutdownReason): Promise<boolean> { label = label || localize('fileOperation', "File operation"); const reasonLabel = reason === ShutdownReason.CLOSE ? localize('closeTheWindow', "Close Window") : reason === ShutdownReason.LOAD ? localize('changeWorkspace', "Change Workspace") : reason === ShutdownReason.RELOAD ? localize('reloadTheWindow', "Reload Window") : localize('quit', "Quit"); @@ -240,3 +273,16 @@ export class BulkEditService implements IBulkEditService { } registerSingleton(IBulkEditService, BulkEditService, true); + +const autoSaveSetting = 'files.refactoring.autoSave'; + +Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({ + id: 'files', + properties: { + [autoSaveSetting]: { + description: localize('refactoring.autoSave', "Controls if files that were part of a refactoring are saved automatically"), + default: true, + type: 'boolean' + } + } +}); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index b3247bba89d..86de76556d2 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -16,7 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { flatten, tail } from 'vs/base/common/arrays'; +import { tail } from 'vs/base/common/arrays'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IFileOperation { @@ -51,7 +51,7 @@ class RenameOperation implements IFileOperation { ) { } get uris() { - return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); + return this._edits.map(edit => [edit.newUri, edit.oldUri]).flat(); } async perform(token: CancellationToken): Promise<IFileOperation> { @@ -105,7 +105,7 @@ class CopyOperation implements IFileOperation { ) { } get uris() { - return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri])); + return this._edits.map(edit => [edit.newUri, edit.oldUri]).flat(); } async perform(token: CancellationToken): Promise<IFileOperation> { @@ -293,7 +293,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { readonly operations: IFileOperation[], readonly confirmBeforeUndo: boolean ) { - this.resources = (<URI[]>[]).concat(...operations.map(op => op.uris)); + this.resources = operations.map(op => op.uris).flat(); } async undo(): Promise<void> { @@ -332,7 +332,7 @@ export class BulkFileEdits { @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, ) { } - async apply(): Promise<void> { + async apply(): Promise<readonly URI[]> { const undoOperations: IFileOperation[] = []; const undoRedoInfo = { undoRedoGroupId: this._undoRedoGroup.id }; @@ -350,7 +350,7 @@ export class BulkFileEdits { } if (edits.length === 0) { - return; + return []; } const groups: Array<RenameEdit | CopyEdit | DeleteEdit | CreateEdit>[] = []; @@ -395,6 +395,8 @@ export class BulkFileEdits { this._progress.report(undefined); } - this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource); + const undoRedoElement = new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo); + this._undoRedoService.pushElement(undoRedoElement, this._undoRedoGroup, this._undoRedoSource); + return undoRedoElement.resources; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index b0f3ba726bb..b1ff888c894 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -232,16 +232,17 @@ export class BulkTextEdits { return { canApply: true }; } - async apply(): Promise<void> { + async apply(): Promise<readonly URI[]> { this._validateBeforePrepare(); const tasks = await this._createEditsTasks(); - if (this._token.isCancellationRequested) { - return; - } try { + if (this._token.isCancellationRequested) { + return []; + } + const resources: URI[] = []; const validation = this._validateTasks(tasks); if (!validation.canApply) { throw new Error(`${validation.reason.toString()} has changed in the meantime`); @@ -254,6 +255,7 @@ export class BulkTextEdits { this._undoRedoService.pushElement(singleModelEditStackElement, this._undoRedoGroup, this._undoRedoSource); task.apply(); singleModelEditStackElement.close(); + resources.push(task.model.uri); } this._progress.report(undefined); } else { @@ -267,10 +269,13 @@ export class BulkTextEdits { for (const task of tasks) { task.apply(); this._progress.report(undefined); + resources.push(task.model.uri); } multiModelEditStackElement.close(); } + return resources; + } finally { dispose(tasks); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index db38d82e544..06f90fcfd98 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -6,6 +6,7 @@ import './menuPreventer'; import './accessibility/accessibility'; import './diffEditorHelper'; +import './editorSettingsMigration'; import './inspectKeybindings'; import './largeFileOptimizations'; import './inspectEditorTokens/inspectEditorTokens'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts b/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts new file mode 100644 index 00000000000..6fc9af98508 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; +import { EditorSettingMigration, ISettingsReader, ISettingsWriter } from 'vs/editor/browser/config/migrateOptions'; +import { Disposable } from 'vs/base/common/lifecycle'; + +class EditorSettingsMigration extends Disposable implements IWorkbenchContribution { + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + ) { + super(); + this._register(this._workspaceService.onDidChangeWorkspaceFolders(async (e) => { + for (const folder of e.added) { + await this._migrateEditorSettingsForFolder(folder); + } + })); + this._migrateEditorSettings(); + } + + private async _migrateEditorSettings(): Promise<void> { + await this._migrateEditorSettingsForFolder(undefined); + for (const folder of this._workspaceService.getWorkspace().folders) { + await this._migrateEditorSettingsForFolder(folder); + } + } + + private async _migrateEditorSettingsForFolder(folder: IWorkspaceFolder | undefined): Promise<void> { + await Promise.all(EditorSettingMigration.items.map(migration => this._migrateEditorSettingForFolderAndOverride(migration, { resource: folder?.uri }))); + } + + private async _migrateEditorSettingForFolderAndOverride(migration: EditorSettingMigration, overrides: IConfigurationOverrides): Promise<void> { + const data = this._configurationService.inspect(`editor.${migration.key}`, overrides); + + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'userValue', ConfigurationTarget.USER); + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'userLocalValue', ConfigurationTarget.USER_LOCAL); + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'userRemoteValue', ConfigurationTarget.USER_REMOTE); + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'workspaceFolderValue', ConfigurationTarget.WORKSPACE_FOLDER); + await this._migrateEditorSettingForFolderOverrideAndTarget(migration, overrides, data, 'workspaceValue', ConfigurationTarget.WORKSPACE); + + if (typeof overrides.overrideIdentifier === 'undefined' && typeof data.overrideIdentifiers !== 'undefined') { + for (const overrideIdentifier of data.overrideIdentifiers) { + await this._migrateEditorSettingForFolderAndOverride(migration, { resource: overrides.resource, overrideIdentifier }); + } + } + } + + private async _migrateEditorSettingForFolderOverrideAndTarget(migration: EditorSettingMigration, overrides: IConfigurationOverrides, data: IConfigurationValue<any>, dataKey: keyof IConfigurationValue<any>, target: ConfigurationTarget): Promise<void> { + const value = data[dataKey]; + if (typeof value === 'undefined') { + return; + } + + const writeCalls: [string, any][] = []; + const read: ISettingsReader = (key: string) => this._configurationService.inspect(`editor.${key}`, overrides)[dataKey]; + const write: ISettingsWriter = (key: string, value: any) => writeCalls.push([key, value]); + migration.migrate(value, read, write); + for (const [wKey, wValue] of writeCalls) { + await this._configurationService.updateValue(`editor.${wKey}`, wValue, overrides, target); + } + } +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorSettingsMigration, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 7b54eef8f20..1e220e2de14 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -15,7 +15,7 @@ import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBo import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon } from 'vs/editor/contrib/find/browser/findWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; @@ -26,6 +26,12 @@ const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', " const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match"); const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); +interface IFindOptions { + showOptionButtons?: boolean; + checkImeCompletionState?: boolean; + showResultCount?: boolean; +} + export abstract class SimpleFindWidget extends Widget { private readonly _findInput: FindInput; private readonly _domNode: HTMLElement; @@ -35,16 +41,16 @@ export abstract class SimpleFindWidget extends Widget { private readonly _updateHistoryDelayer: Delayer<void>; private readonly prevBtn: SimpleButton; private readonly nextBtn: SimpleButton; + private _matchesCount: HTMLElement | undefined; private _isVisible: boolean = false; - private foundMatch: boolean = false; + private _foundMatch: boolean = false; constructor( @IContextViewService private readonly _contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, private readonly _state: FindReplaceState = new FindReplaceState(), - showOptionButtons?: boolean, - checkImeCompletionState?: boolean + private readonly _options: IFindOptions ) { super(); @@ -59,20 +65,23 @@ export abstract class SimpleFindWidget extends Widget { new RegExp(value); return null; } catch (e) { - this.foundMatch = false; - this.updateButtons(this.foundMatch); + this._foundMatch = false; + this.updateButtons(this._foundMatch); return { content: e.message }; } } - }, contextKeyService, showOptionButtons)); + }, contextKeyService, _options.showOptionButtons)); // Find History with update delayer this._updateHistoryDelayer = new Delayer<void>(500); - this._register(this._findInput.onInput((e) => { - if (!checkImeCompletionState || !this._findInput.isImeSessionInProgress) { - this.foundMatch = this._onInputChanged(); - this.updateButtons(this.foundMatch); + this._register(this._findInput.onInput(async (e) => { + if (!_options.checkImeCompletionState || !this._findInput.isImeSessionInProgress) { + this._foundMatch = this._onInputChanged(); + if (this._options.showResultCount) { + await this.updateResultCount(); + } + this.updateButtons(this._foundMatch); this.focusFindBox(); this._delayedUpdateHistory(); } @@ -152,6 +161,14 @@ export abstract class SimpleFindWidget extends Widget { this._register(dom.addDisposableListener(this._innerDomNode, 'click', (event) => { event.stopPropagation(); })); + + if (_options?.showResultCount) { + this._domNode.classList.add('result-count'); + this._register(this._findInput.onDidChange(() => { + this.updateResultCount(); + this.updateButtons(this._foundMatch); + })); + } } protected abstract _onInputChanged(): boolean; @@ -161,6 +178,7 @@ export abstract class SimpleFindWidget extends Widget { protected abstract _onFocusTrackerBlur(): void; protected abstract _onFindInputFocusTrackerFocus(): void; protected abstract _onFindInputFocusTrackerBlur(): void; + protected abstract _getResultCount(): Promise<{ resultIndex: number; resultCount: number } | undefined>; protected get inputValue() { return this._findInput.getValue(); @@ -214,7 +232,7 @@ export abstract class SimpleFindWidget extends Widget { } this._isVisible = true; - this.updateButtons(this.foundMatch); + this.updateButtons(this._foundMatch); setTimeout(() => { this._innerDomNode.classList.add('visible', 'visible-transition'); @@ -243,7 +261,7 @@ export abstract class SimpleFindWidget extends Widget { // Need to delay toggling visibility until after Transition, then visibility hidden - removes from tabIndex list setTimeout(() => { this._isVisible = false; - this.updateButtons(this.foundMatch); + this.updateButtons(this._foundMatch); this._innerDomNode.classList.remove('visible'); }, 200); } @@ -281,6 +299,20 @@ export abstract class SimpleFindWidget extends Widget { this.nextBtn.focus(); this._findInput.inputBox.focus(); } + + async updateResultCount(): Promise<void> { + const count = await this._getResultCount(); + if (!this._matchesCount) { + this._matchesCount = document.createElement('div'); + this._matchesCount.className = 'matchesCount'; + } + this._matchesCount.innerText = ''; + const label = count === undefined || count.resultCount === 0 ? `No Results` : `${count.resultIndex + 1} of ${count.resultCount}`; + this._matchesCount.appendChild(document.createTextNode(label)); + this._matchesCount.classList.toggle('no-results', !count || count.resultCount === 0); + this._findInput?.domNode.insertAdjacentElement('afterend', this._matchesCount); + this._foundMatch = !!count && count.resultCount > 0; + } } // theming @@ -299,4 +331,9 @@ registerThemingParticipant((theme, collector) => { if (widgetShadowColor) { collector.addRule(`.monaco-workbench .simple-find-part { box-shadow: 0 0 8px 2px ${widgetShadowColor}; }`); } + + const error = theme.getColor(errorForeground); + if (error) { + collector.addRule(`.no-results.matchesCount { color: ${error}; }`); + } }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts index a3162f0ffa2..53dc09e2826 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { CharacterPair, CommentRule, EnterAction, ExplicitLanguageConfiguration, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -89,7 +89,8 @@ export class LanguageConfigurationFileHandler extends Disposable { @ITextMateService textMateService: ITextMateService, @ILanguageService private readonly _languageService: ILanguageService, @IExtensionResourceLoaderService private readonly _extensionResourceLoaderService: IExtensionResourceLoaderService, - @IExtensionService private readonly _extensionService: IExtensionService + @IExtensionService private readonly _extensionService: IExtensionService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(); @@ -414,7 +415,7 @@ export class LanguageConfigurationFileHandler extends Disposable { __electricCharacterSupport: undefined, }; - LanguageConfigurationRegistry.register(languageId, richEditConfig, 50); + this._languageConfigurationService.register(languageId, richEditConfig, 50); } private _parseRegex(languageId: string, confPath: string, value: string | IRegExp): RegExp | undefined { diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index fbd43cd3aed..d99f9c41a0b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -45,6 +45,7 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { private _domNode: HTMLElement; private _body: HTMLElement; private _md: HTMLElement | undefined; + private _plainText: HTMLElement | undefined; private _clearTimeout: any; private _editAction: Action | null = null; @@ -125,8 +126,10 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { private updateCommentBody(body: string | IMarkdownString) { this._body.innerText = ''; this._md = undefined; + this._plainText = undefined; if (typeof body === 'string') { - this._body.innerText = body; + this._plainText = dom.append(this._body, dom.$('.comment-body-plainstring')); + this._plainText.innerText = body; } else { this._md = this.markdownRenderer.render(body).element; this._body.appendChild(this._md); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index ac743f0f7bb..b4989b77691 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -311,7 +311,6 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends content.push(`.review-widget .body code { font-family: var(${fontFamilyVar}); font-weight: var(${fontWeightVar}); - font-size: var(${fontSizeVar}); }`); this._styleElement.textContent = content.join('\n'); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 791a2b70e7b..2539c3e3960 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -108,6 +108,10 @@ padding: 0 2px 0 2px; } +.review-widget .body .review-comment .review-comment-contents .comment-body .comment-body-plainstring { + white-space: pre; +} + .review-widget .body .review-comment .review-comment-contents .comment-body { padding-top: 4px; } diff --git a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts index 12411801f21..5ec8c29bae1 100644 --- a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts +++ b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts @@ -17,15 +17,15 @@ import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statu export const STATUS_BAR_DEBUGGING_BACKGROUND = registerColor('statusBar.debuggingBackground', { dark: '#CC6633', light: '#CC6633', - hcDark: '#CC6633', - hcLight: '#CC6633' + hcDark: '#BA592C', + hcLight: '#B5200D' }, localize('statusBarDebuggingBackground', "Status bar background color when a program is being debugged. The status bar is shown in the bottom of the window")); export const STATUS_BAR_DEBUGGING_FOREGROUND = registerColor('statusBar.debuggingForeground', { dark: STATUS_BAR_FOREGROUND, light: STATUS_BAR_FOREGROUND, hcDark: STATUS_BAR_FOREGROUND, - hcLight: STATUS_BAR_FOREGROUND + hcLight: '#FFFFFF' }, localize('statusBarDebuggingForeground', "Status bar foreground color when a program is being debugged. The status bar is shown in the bottom of the window")); export const STATUS_BAR_DEBUGGING_BORDER = registerColor('statusBar.debuggingBorder', { diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index d25efaffb6e..c27c88f27a6 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -6,6 +6,7 @@ import * as cp from 'child_process'; import { getDriveLetter } from 'vs/base/common/extpath'; import * as platform from 'vs/base/common/platform'; +// import { IProcessTreeNode } from 'windows-process-tree'; function spawnAsPromised(command: string, args: string[]): Promise<string> { return new Promise((resolve, reject) => { @@ -32,8 +33,8 @@ export async function hasChildProcesses(processId: number | undefined): Promise< if (platform.isWindows) { const windowsProcessTree = await import('windows-process-tree'); return new Promise<boolean>(resolve => { - windowsProcessTree.getProcessTree(processId, (processTree) => { - resolve(processTree.children.length > 0); + windowsProcessTree.getProcessTree(processId, processTree => { + resolve(!!processTree && processTree.children.length > 0); }); }); } else { diff --git a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts index f8735e39e90..fdcf9b5c181 100644 --- a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -7,7 +7,7 @@ import { IGrammarContributions, EmmetEditorAction } from 'vs/workbench/contrib/e import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; +import { ILanguageService } from 'vs/editor/common/languages/language'; class MockGrammarContributions implements IGrammarContributions { private scopeName: string; @@ -24,13 +24,14 @@ class MockGrammarContributions implements IGrammarContributions { suite('Emmet', () => { test('Get language mode and parent mode for emmet', () => { - withTestCodeEditor([], {}, (editor) => { + withTestCodeEditor([], {}, (editor, viewModel, instantiationService) => { + const languageService = instantiationService.get(ILanguageService); const disposables = new DisposableStore(); - disposables.add(ModesRegistry.registerLanguage({ id: 'markdown' })); - disposables.add(ModesRegistry.registerLanguage({ id: 'handlebars' })); - disposables.add(ModesRegistry.registerLanguage({ id: 'nunjucks' })); - disposables.add(ModesRegistry.registerLanguage({ id: 'laravel-blade' })); + disposables.add(languageService.registerLanguage({ id: 'markdown' })); + disposables.add(languageService.registerLanguage({ id: 'handlebars' })); + disposables.add(languageService.registerLanguage({ id: 'nunjucks' })); + disposables.add(languageService.registerLanguage({ id: 'laravel-blade' })); function testIsEnabled(mode: string, scopeName: string, expectedLanguage?: string, expectedParentLanguage?: string) { const model = editor.getModel(); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 0d0c23cdc12..41515d1c2e2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -74,6 +74,8 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Event } from 'vs/base/common/event'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { UnsupportedExtensionsMigrationContrib } from 'vs/workbench/contrib/extensions/browser/unsupportedExtensionsMigrationContribution'; +import { isWeb } from 'vs/base/common/platform'; +import { ExtensionsCleaner } from 'vs/workbench/contrib/extensions/browser/extensionsCleaner'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -1557,6 +1559,10 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionDependencyChecker, Life workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementWorkspaceTrustTransitionParticipant, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(ExtensionsCompletionItemsProvider, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(UnsupportedExtensionsMigrationContrib, LifecyclePhase.Eventually); +if (isWeb) { + workbenchRegistry.registerWorkbenchContribution(ExtensionsCleaner, LifecyclePhase.Eventually); +} + // Running Extensions registerAction2(ShowRuntimeExtensionsAction); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts b/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts new file mode 100644 index 00000000000..2d9b7872491 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; + +export class ExtensionsCleaner implements IWorkbenchContribution { + + constructor( + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IStorageService storageService: IStorageService, + ) { + ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService); + } +} diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 1ee398a691a..f7eca9c59df 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -7,10 +7,10 @@ import { localize } from 'vs/nls'; import { assertIsDefined } from 'vs/base/common/types'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { toAction } from 'vs/base/common/actions'; -import { VIEWLET_ID, TEXT_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, BINARY_TEXT_FILE_MODE } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { IEditorOpenContext, EditorInputCapabilities, isTextEditorViewState } from 'vs/workbench/common/editor'; +import { IEditorOpenContext, EditorInputCapabilities, isTextEditorViewState, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IErrorWithActions } from 'vs/base/common/errorMessage'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; @@ -212,8 +212,9 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { const defaultBinaryEditor = this.configurationService.getValue<string | undefined>('workbench.editor.defaultBinaryEditor'); - const groupToOpen = this.group ?? this.editorGroupService.activeGroup; - const editorOptions = { + const group = this.group ?? this.editorGroupService.activeGroup; + + let editorOptions = { ...options, // Make sure to not steal away the currently active group // because we are triggering another openEditor() call @@ -222,20 +223,43 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { activation: EditorActivation.PRESERVE }; - // If we the user setting specifies a default binary editor we use that - if (defaultBinaryEditor && defaultBinaryEditor !== '') { - this.editorService.replaceEditors([{ - editor: input, - replacement: { resource: input.resource, options: { ...editorOptions, override: defaultBinaryEditor } } - }], groupToOpen); + // Check configuration and determine whether we open the binary + // file input in a different editor or going through the same + // editor. + // Going through the same editor is debt, and a better solution + // would be to introduce a real editor for the binary case + // and avoid enforcing binary or text on the file editor input. + + if (defaultBinaryEditor && defaultBinaryEditor !== '' && defaultBinaryEditor !== DEFAULT_EDITOR_ASSOCIATION.id) { + this.doOpenAsBinaryInDifferentEditor(group, defaultBinaryEditor, input, editorOptions); + } else { + this.doOpenAsBinaryInSameEditor(group, defaultBinaryEditor, input, editorOptions); } + } - // Otherwise we mark file input for forced binary opening and reopen the file - else { - input.setForceOpenAsBinary(); + private doOpenAsBinaryInDifferentEditor(group: IEditorGroup, editorId: string | undefined, editor: FileEditorInput, editorOptions: ITextEditorOptions): void { + this.editorService.replaceEditors([{ + editor, + replacement: { resource: editor.resource, options: { ...editorOptions, override: editorId } } + }], group); + } + + private doOpenAsBinaryInSameEditor(group: IEditorGroup, editorId: string | undefined, editor: FileEditorInput, editorOptions: ITextEditorOptions): void { - groupToOpen.openEditor(input, editorOptions); + // Open binary as text + if (editorId === DEFAULT_EDITOR_ASSOCIATION.id) { + editor.setForceOpenAsText(); + editor.setPreferredLanguageId(BINARY_TEXT_FILE_MODE); // https://github.com/microsoft/vscode/issues/131076 + + editorOptions = { ...editorOptions, forceReload: true }; // Same pane and same input, must force reload to clear cached state } + + // Open as binary + else { + editor.setForceOpenAsBinary(); + } + + group.openEditor(editor, editorOptions); } private async openAsFolder(input: FileEditorInput): Promise<void> { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 317f7f2da0d..1a6f2c8f672 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -293,6 +293,11 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'description': nls.localize('files.simpleDialog.enable', "Enables the simple file dialog. The simple file dialog replaces the system file dialog when enabled."), 'default': false + }, + 'files.participants.timeout': { + type: 'number', + default: 60000, + markdownDescription: nls.localize('files.participants.timeout', "Timeout in milliseconds after which file participants for create, rename, and delete are cancelled. Use `0` to disable participants."), } } }); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 55fb3414ea4..e34b213a567 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -14,7 +14,7 @@ import { EncodingMode, TextFileOperationError, TextFileOperationResult } from 'v import { FileOperationResult, FileOperationError, NotModifiedSinceFileOperationError, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { timeout } from 'vs/base/common/async'; -import { ModesRegistry, PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -171,7 +171,7 @@ suite('Files - FileEditorInput', () => { test('preferred language', async function () { const languageId = 'file-input-test'; - ModesRegistry.registerLanguage({ + const registration = accessor.languageService.registerLanguage({ id: languageId, }); @@ -190,6 +190,8 @@ suite('Files - FileEditorInput', () => { const model2 = await input2.resolve() as TextFileEditorModel; assert.strictEqual(model2.textEditorModel!.getLanguageId(), languageId); + + registration.dispose(); }); test('preferred contents', async function () { diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 99d8c6897b6..52fe78d35bb 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -19,7 +19,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { editorBackground, editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser/interactiveEditorInput'; import { CodeCellLayoutChangeEvent, IActiveNotebookEditorDelegate, ICellViewModel, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -54,9 +54,10 @@ import { ModesHoverController } from 'vs/editor/contrib/hover/browser/hover'; import { MarkerController } from 'vs/editor/contrib/gotoError/browser/gotoError'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { ITextEditorOptions, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NOTEBOOK_KERNEL } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -110,6 +111,8 @@ export class InteractiveEditor extends EditorPane { #onDidFocusWidget = this._register(new Emitter<void>()); override get onDidFocus(): Event<void> { return this.#onDidFocusWidget.event; } + #onDidChangeSelection = this._register(new Emitter<IEditorPaneSelectionChangeEvent>()); + readonly onDidChangeSelection = this.#onDidChangeSelection.event; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -436,6 +439,10 @@ export class InteractiveEditor extends EditorPane { } })); + this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeCursorPosition(e => this.#onDidChangeSelection.fire({ reason: this.#toEditorPaneSelectionChangeReason(e) }))); + this.#widgetDisposableStore.add(this.#codeEditorWidget.onDidChangeModelContent(() => this.#onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); + + this.#widgetDisposableStore.add(this.#notebookKernelService.onDidChangeNotebookAffinity(this.#syncWithKernel, this)); this.#widgetDisposableStore.add(this.#notebookKernelService.onDidChangeSelectedNotebooks(this.#syncWithKernel, this)); @@ -495,6 +502,15 @@ export class InteractiveEditor extends EditorPane { this.#syncWithKernel(); } + #toEditorPaneSelectionChangeReason(e: ICursorPositionChangedEvent): EditorPaneSelectionChangeReason { + switch (e.source) { + case TextEditorSelectionSource.PROGRAMMATIC: return EditorPaneSelectionChangeReason.PROGRAMMATIC; + case TextEditorSelectionSource.NAVIGATION: return EditorPaneSelectionChangeReason.NAVIGATION; + case TextEditorSelectionSource.JUMP: return EditorPaneSelectionChangeReason.JUMP; + default: return EditorPaneSelectionChangeReason.USER; + } + } + #lastCell: ICellViewModel | undefined = undefined; #lastCellDisposable = new DisposableStore(); #state: ScrollingState = ScrollingState.Initial; @@ -565,7 +581,11 @@ export class InteractiveEditor extends EditorPane { return; } - if (this.#lastCell instanceof CodeCellViewModel && (e as CodeCellLayoutChangeEvent).outputHeight === undefined && !this.#notebookWidget.value!.isScrolledToBottom()) { + if (!this.#notebookWidget.value) { + return; + } + + if (this.#lastCell instanceof CodeCellViewModel && (e as CodeCellLayoutChangeEvent).outputHeight === undefined && !this.#notebookWidget.value.isScrolledToBottom()) { return; } diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index d79b71400e5..d6d4689d90e 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -41,6 +41,21 @@ class LanguageStatusViewModel { } } +class StoredCounter { + + constructor(@IStorageService private readonly _storageService: IStorageService, private readonly _key: string) { } + + get value() { + return this._storageService.getNumber(this._key, StorageScope.GLOBAL, 0); + } + + increment(): number { + const n = this.value + 1; + this._storageService.store(this._key, n, StorageScope.GLOBAL, StorageTarget.MACHINE); + return n; + } +} + class EditorStatusContribution implements IWorkbenchContribution { private static readonly _id = 'status.languageStatus'; @@ -48,6 +63,7 @@ class EditorStatusContribution implements IWorkbenchContribution { private static readonly _keyDedicatedItems = 'languageStatus.dedicated'; private readonly _disposables = new DisposableStore(); + private readonly _interactionCounter: StoredCounter; private _dedicated = new Set<string>(); @@ -65,6 +81,7 @@ class EditorStatusContribution implements IWorkbenchContribution { ) { _storageService.onDidChangeValue(this._handleStorageChange, this, this._disposables); this._restoreState(); + this._interactionCounter = new StoredCounter(_storageService, 'languageStatus.interactCount'); _languageStatusService.onDidChange(this._update, this, this._disposables); _editorService.onDidActiveEditorChange(this._update, this, this._disposables); @@ -181,6 +198,37 @@ class EditorStatusContribution implements IWorkbenchContribution { } else { this._combinedEntry.update(props); } + + // animate the status bar icon whenever language status changes, repeat animation + // when severity is warning or error, don't show animation when showing progress/busy + const userHasInteractedWithStatus = this._interactionCounter.value >= 3; + const node = document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus span.codicon'); + if (node instanceof HTMLElement) { + const _wiggle = 'wiggle'; + const _repeat = 'repeat'; + if (!isOneBusy) { + node.classList.toggle(_wiggle, showSeverity || !userHasInteractedWithStatus); + node.classList.toggle(_repeat, showSeverity); + this._renderDisposables.add(dom.addDisposableListener(node, 'animationend', _e => node.classList.remove(_wiggle, _repeat))); + } else { + node.classList.remove(_wiggle, _repeat); + } + } + + // track when the hover shows (this is automagic and DOM mutation spying is needed...) + // use that as signal that the user has interacted/learned language status items work + if (!userHasInteractedWithStatus) { + const hoverTarget = document.querySelector('.monaco-workbench .context-view'); + if (hoverTarget instanceof HTMLElement) { + const observer = new MutationObserver(() => { + if (document.contains(element)) { + this._interactionCounter.increment(); + observer.disconnect(); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + } + } } // dedicated status bar items are shows as-is in the status bar diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 8021133a7a1..131695f460a 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -3,9 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .statusbar DIV#status\.languageStatus .codicon-circle-large-filled::before { - font-size: 12px; - line-height: 18px; + +@keyframes wiggle { + 0% { + transform: rotate(0) scale(1) + } + + 15%, + 45% { + transform: rotate(.04turn) scale(1.1) + } + + 30%, + 60% { + transform: rotate(-.04turn) scale(1.2) + } + + 100% { + transform: rotate(0) scale(1) + } +} + +.monaco-workbench.enable-motion .statusbar DIV#status\.languageStatus span.codicon.wiggle { + animation-duration: .8s; + animation-iteration-count: 1; + animation-name: wiggle; +} + +.monaco-workbench.enable-motion .statusbar DIV#status\.languageStatus span.codicon.wiggle.repeat { + animation-iteration-count: 3; } .monaco-workbench .hover-language-status { @@ -17,61 +43,61 @@ border-bottom: 1px solid var(--vscode-notifications-border); } -.monaco-workbench .hover-language-status > .severity { +.monaco-workbench .hover-language-status>.severity { padding-right: 8px; flex: 1; margin: auto; display: none; } -.monaco-workbench .hover-language-status > .severity.sev3 { +.monaco-workbench .hover-language-status>.severity.sev3 { color: var(--vscode-notificationsErrorIcon-foreground) } -.monaco-workbench .hover-language-status > .severity.sev2 { +.monaco-workbench .hover-language-status>.severity.sev2 { color: var(--vscode-notificationsInfoIcon-foreground) } -.monaco-workbench .hover-language-status > .severity.show { +.monaco-workbench .hover-language-status>.severity.show { display: inherit; } -.monaco-workbench .hover-language-status > .element { +.monaco-workbench .hover-language-status>.element { display: flex; justify-content: space-between; vertical-align: middle; flex-grow: 100; } -.monaco-workbench .hover-language-status > .element > .left > .detail:not(:empty)::before { +.monaco-workbench .hover-language-status>.element>.left>.detail:not(:empty)::before { /* allow-any-unicode-next-line */ content: '–'; padding: 0 4px; opacity: 0.6; } -.monaco-workbench .hover-language-status > .element > .left > .label:empty { +.monaco-workbench .hover-language-status>.element>.left>.label:empty { display: none; } -.monaco-workbench .hover-language-status > .element .left { +.monaco-workbench .hover-language-status>.element .left { margin: auto 0; } -.monaco-workbench .hover-language-status > .element .right { +.monaco-workbench .hover-language-status>.element .right { margin: auto 0; display: flex; } -.monaco-workbench .hover-language-status > .element .right:not(:empty) { +.monaco-workbench .hover-language-status>.element .right:not(:empty) { padding-left: 16px; } -.monaco-workbench .hover-language-status > .element .right .monaco-link { +.monaco-workbench .hover-language-status>.element .right .monaco-link { margin: auto 0; white-space: nowrap; } -.monaco-workbench .hover-language-status > .element .right .monaco-action-bar:not(:first-child) { +.monaco-workbench .hover-language-status>.element .right .monaco-action-bar:not(:first-child) { padding-left: 8px; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts index 66037ed429c..7e3796f5342 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten } from 'vs/base/common/arrays'; import { disposableTimeout, Throttler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -119,7 +118,7 @@ class CellStatusBarHelper extends Disposable { return; } - const items = flatten(itemLists.map(itemList => itemList.items)); + const items = itemLists.map(itemList => itemList.items).flat(); const newIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); this._currentItemLists.forEach(itemList => itemList.dispose && itemList.dispose()); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 90a05458d9e..10037c96943 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -131,7 +131,7 @@ export function runPasteCells(editor: INotebookEditor, activeCell: ICellViewMode kind: SelectionStateType.Index, focus: { start: newFocusIndex, end: newFocusIndex + 1 }, selections: [{ start: newFocusIndex, end: newFocusIndex + pasteCells.items.length }] - }), undefined); + }), undefined, true); } else { if (editor.getLength() !== 0) { return false; @@ -148,7 +148,7 @@ export function runPasteCells(editor: INotebookEditor, activeCell: ICellViewMode kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 1, end: pasteCells.items.length + 1 }] - }), undefined); + }), undefined, true); } return true; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index 6b4c1aeb914..cb7ab0f8586 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -279,7 +279,7 @@ class ImplictKernelSelector implements IDisposable { case NotebookCellsChangeType.ChangeCellContent: case NotebookCellsChangeType.ModelChange: case NotebookCellsChangeType.Move: - case NotebookCellsChangeType.ChangeLanguage: + case NotebookCellsChangeType.ChangeCellLanguage: logService.trace('IMPLICIT kernel selection because of change event', event.kind); selectKernel(); break; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index 99a308a9bd4..662b1929a2e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -41,6 +41,7 @@ import { isEqual } from 'vs/base/common/resources'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; let MAX_MATCHES_COUNT_WIDTH = 69; +const PROGRESS_BAR_DELAY = 200; // show progress for at least 200ms export class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEditorContribution { static id: string = 'workbench.notebook.find'; @@ -76,9 +77,9 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote if (e.isSearching) { if (this._state.isSearching) { - this._progressBar.infinite().show(); + this._progressBar.infinite().show(PROGRESS_BAR_DELAY); } else { - this._progressBar.stop(); + this._progressBar.stop().hide(); } } @@ -88,7 +89,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } const matches = this._findModel.findMatches; - this._replaceAllBtn.setEnabled(matches.find(match => match.modelMatchCount < match.matches.length) === null); + this._replaceAllBtn.setEnabled(matches.length > 0 && matches.find(match => match.modelMatchCount < match.matches.length) === undefined); if (e.filters) { this._findInput.updateFilterState((this._state.filters?.markupPreview ?? false) || (this._state.filters?.codeOutput ?? false)); @@ -148,7 +149,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote if (currentMatch.isModelMatch) { const match = currentMatch.match as FindMatch; - this._progressBar.infinite().show(); + this._progressBar.infinite().show(PROGRESS_BAR_DELAY); const replacePattern = this.replacePattern; const replaceString = replacePattern.buildReplaceString(match.matches, this._state.preserveCase); @@ -168,7 +169,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote return; } - this._progressBar.infinite().show(); + this._progressBar.infinite().show(PROGRESS_BAR_DELAY); const replacePattern = this.replacePattern; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 38c9042b175..9e7e660b6b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -34,6 +34,7 @@ import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; +import { isSafari } from 'vs/base/common/platform'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); @@ -73,57 +74,73 @@ class NotebookFindFilterActionViewItem extends DropdownMenuActionViewItem { } private getActions(): IAction[] { - return [ - { - checked: this.filters.markupInput, - class: undefined, - enabled: !this.filters.markupPreview, - id: 'findInMarkdownInput', - label: NOTEBOOK_FIND_IN_MARKUP_INPUT, - run: async () => { - this.filters.markupInput = !this.filters.markupInput; - }, - tooltip: '', - dispose: () => null + const markdownInput: IAction = { + checked: this.filters.markupInput, + class: undefined, + enabled: !this.filters.markupPreview, + id: 'findInMarkdownInput', + label: NOTEBOOK_FIND_IN_MARKUP_INPUT, + run: async () => { + this.filters.markupInput = !this.filters.markupInput; }, - { - checked: this.filters.markupPreview, - class: undefined, - enabled: true, - id: 'findInMarkdownInput', - label: NOTEBOOK_FIND_IN_MARKUP_PREVIEW, - run: async () => { - this.filters.markupPreview = !this.filters.markupPreview; - }, - tooltip: '', - dispose: () => null + tooltip: '', + dispose: () => null + }; + + const markdownPreview: IAction = { + checked: this.filters.markupPreview, + class: undefined, + enabled: true, + id: 'findInMarkdownInput', + label: NOTEBOOK_FIND_IN_MARKUP_PREVIEW, + run: async () => { + this.filters.markupPreview = !this.filters.markupPreview; }, - new Separator(), - { - checked: this.filters.codeInput, - class: undefined, - enabled: true, - id: 'findInCodeInput', - label: NOTEBOOK_FIND_IN_CODE_INPUT, - run: async () => { - this.filters.codeInput = !this.filters.codeInput; - }, - tooltip: '', - dispose: () => null + tooltip: '', + dispose: () => null + }; + + const codeInput: IAction = { + checked: this.filters.codeInput, + class: undefined, + enabled: true, + id: 'findInCodeInput', + label: NOTEBOOK_FIND_IN_CODE_INPUT, + run: async () => { + this.filters.codeInput = !this.filters.codeInput; }, - { - checked: this.filters.codeOutput, - class: undefined, - enabled: true, - id: 'findInCodeOutput', - label: NOTEBOOK_FIND_IN_CODE_OUTPUT, - run: async () => { - this.filters.codeOutput = !this.filters.codeOutput; - }, - tooltip: '', - dispose: () => null + tooltip: '', + dispose: () => null + }; + + const codeOutput = { + checked: this.filters.codeOutput, + class: undefined, + enabled: true, + id: 'findInCodeOutput', + label: NOTEBOOK_FIND_IN_CODE_OUTPUT, + run: async () => { + this.filters.codeOutput = !this.filters.codeOutput; }, - ]; + tooltip: '', + dispose: () => null + }; + + if (isSafari) { + return [ + markdownInput, + codeInput + ]; + } else { + return [ + markdownInput, + markdownPreview, + new Separator(), + codeInput, + codeOutput, + ]; + } + } override updateChecked(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 239aa8064cf..6d2e6f4785e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -23,7 +23,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Progress } from 'vs/platform/progress/common/progress'; -import { flatten } from 'vs/base/common/arrays'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; // format notebook @@ -91,7 +90,7 @@ registerAction2(class extends Action2 { return []; })); - await bulkEditService.apply(/* edit */flatten(allCellEdits), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); + await bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); } finally { disposable.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 44ffbe69d1f..473ff42494f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -430,11 +430,12 @@ export class NotebookCellOutline extends Disposable implements IOutline<OutlineE // cap the amount of characters that we look at and use the following logic // - for MD prefer headings (each header is an entry) // - otherwise use the first none-empty line of the cell (MD or code) - let content = cell.getText().substring(0, 10_000); + let content = this._getCellFirstNonEmptyLine(cell); let hasHeader = false; if (isMarkdown) { - for (const token of marked.lexer(content, { gfm: true })) { + const fullContent = cell.getText().substring(0, 10_000); + for (const token of marked.lexer(fullContent, { gfm: true })) { if (token.type === 'heading') { hasHeader = true; entries.push(new OutlineEntry(entries.length, token.depth, cell, renderMarkdownAsPlaintext({ value: token.text }).trim(), false, false)); @@ -558,6 +559,19 @@ export class NotebookCellOutline extends Disposable implements IOutline<OutlineE } } + private _getCellFirstNonEmptyLine(cell: ICellViewModel) { + const textBuffer = cell.textBuffer; + for (let i = 0; i < textBuffer.getLineCount(); i++) { + const firstNonWhitespace = textBuffer.getLineFirstNonWhitespaceColumn(i + 1); + const lineLength = textBuffer.getLineLength(i + 1); + if (firstNonWhitespace < lineLength) { + return textBuffer.getLineContent(i + 1); + } + } + + return cell.getText().substring(0, 10_000); + } + get isEmpty(): boolean { return this._entries.length === 0; } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index 8e6f97a2216..d0c6379a54b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts @@ -10,7 +10,7 @@ import { isDocumentExcludePattern, TransientCellMetadata, TransientDocumentMetad import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, args): { +CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor): { viewType: string; displayName: string; options: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata }; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts index 66f576aa3f4..f4209f52ad9 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOperations.ts @@ -143,7 +143,7 @@ export function runDeleteAction(editor: IActiveNotebookEditor, cell: ICellViewMo return { kind: SelectionStateType.Index, focus: { start: 0, end: 0 }, selections: [{ start: 0, end: 0 }] }; } } - }, undefined); + }, undefined, true); } else { const focus = editor.getFocus(); const edits: ICellReplaceEdit[] = [{ @@ -169,14 +169,14 @@ export function runDeleteAction(editor: IActiveNotebookEditor, cell: ICellViewMo textModel.applyEdits(edits, true, { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: finalSelections - }), undefined); + }), undefined, true); } else { // users decide to delete a cell out of current focus/selection const newFocus = focus.start > targetCellIndex ? { start: focus.start - 1, end: focus.end - 1 } : focus; textModel.applyEdits(edits, true, { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: finalSelections - }), undefined); + }), undefined, true); } } } @@ -222,7 +222,8 @@ export async function moveCellRange(context: INotebookCellActionContext, directi selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: [finalSelection] }), - undefined + undefined, + true ); const focusRange = editor.getSelections()[0] ?? editor.getFocus(); editor.revealCellRangeInView(focusRange); @@ -250,7 +251,8 @@ export async function moveCellRange(context: INotebookCellActionContext, directi selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: [finalSelection] }), - undefined + undefined, + true ); const focusRange = editor.getSelections()[0] ?? editor.getFocus(); @@ -304,7 +306,8 @@ export async function copyCellRange(context: INotebookCellActionContext, directi selections: selections }, () => ({ kind: SelectionStateType.Index, focus: focus, selections: selections }), - undefined + undefined, + true ); } else { // insert down, move selections @@ -328,7 +331,8 @@ export async function copyCellRange(context: INotebookCellActionContext, directi selections: selections }, () => ({ kind: SelectionStateType.Index, focus: newFocus, selections: newSelections }), - undefined + undefined, + true ); const focusRange = editor.getSelections()[0] ?? editor.getFocus(); @@ -624,10 +628,10 @@ export function insertCell( const insertIndex = cell ? (direction === 'above' ? index : nextIndex) : index; - return insertCellAtIndex(viewModel, insertIndex, initialText, language, type, undefined, [], true); + return insertCellAtIndex(viewModel, insertIndex, initialText, language, type, undefined, [], true, true); } -export function insertCellAtIndex(viewModel: NotebookViewModel, index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, outputs: IOutputDto[], synchronous: boolean, pushUndoStop: boolean = true): CellViewModel { +export function insertCellAtIndex(viewModel: NotebookViewModel, index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, outputs: IOutputDto[], synchronous: boolean, pushUndoStop: boolean): CellViewModel { const endSelections: ISelectionState = { kind: SelectionStateType.Index, focus: { start: index, end: index + 1 }, selections: [{ start: index, end: index + 1 }] }; viewModel.notebookDocument.applyEdits([ { @@ -648,29 +652,3 @@ export function insertCellAtIndex(viewModel: NotebookViewModel, index: number, s ], synchronous, { kind: SelectionStateType.Index, focus: viewModel.getFocus(), selections: viewModel.getSelections() }, () => endSelections, undefined, pushUndoStop); return viewModel.cellAt(index)!; } - - -/** - * - * @param index - * @param length - * @param newIdx in an index scheme for the state of the tree after the current cell has been "removed" - * @param synchronous - * @param pushedToUndoStack - */ -export function moveCellToIdx(editor: IActiveNotebookEditor, index: number, length: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean { - const viewCell = editor.cellAt(index) as CellViewModel | undefined; - if (!viewCell) { - return false; - } - - editor.textModel.applyEdits([ - { - editType: CellEditType.Move, - index, - length, - newIdx - } - ], synchronous, { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: { start: newIdx, end: newIdx + 1 }, selections: [{ start: newIdx, end: newIdx + 1 }] }), undefined); - return true; -} diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 602e14a3940..588675f6403 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -208,7 +208,7 @@ registerAction2(class ClearCellOutputsAction extends NotebookCellAction { return; } - editor.textModel.applyEdits([{ editType: CellEditType.Output, index, outputs: [] }], true, undefined, () => undefined, undefined); + editor.textModel.applyEdits([{ editType: CellEditType.Output, index, outputs: [] }], true, undefined, () => undefined, undefined, true); const runState = notebookExecutionStateService.getCellExecution(context.cell.uri)?.state; if (runState !== NotebookCellExecutionState.Executing) { @@ -220,7 +220,7 @@ registerAction2(class ClearCellOutputsAction extends NotebookCellAction { executionOrder: null, lastRunSuccess: null } - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); } } }); @@ -266,7 +266,7 @@ registerAction2(class ClearAllCellOutputsAction extends NotebookAction { editor.textModel.applyEdits( editor.textModel.cells.map((cell, index) => ({ editType: CellEditType.Output, index, outputs: [] - })), true, undefined, () => undefined, undefined); + })), true, undefined, () => undefined, undefined, true); const clearExecutionMetadataEdits = editor.textModel.cells.map((cell, index) => { const runState = notebookExecutionStateService.getCellExecution(cell.uri)?.state; @@ -285,7 +285,7 @@ registerAction2(class ClearAllCellOutputsAction extends NotebookAction { } }).filter(edit => !!edit) as ICellEditOperation[]; if (clearExecutionMetadataEdits.length) { - context.notebookEditor.textModel.applyEdits(clearExecutionMetadataEdits, true, undefined, () => undefined, undefined); + context.notebookEditor.textModel.applyEdits(clearExecutionMetadataEdits, true, undefined, () => undefined, undefined, true); } } }); @@ -451,7 +451,7 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction<ICellR const index = context.notebookEditor.textModel.cells.indexOf(context.cell.model); context.notebookEditor.textModel.applyEdits( [{ editType: CellEditType.CellLanguage, index, language: languageId }], - true, undefined, () => undefined, undefined + true, undefined, () => undefined, undefined, true ); } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 5e007b57a28..89c87eb81e7 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -517,7 +517,7 @@ abstract class AbstractElementRenderer extends Disposable { this.notebookEditor.textModel!.applyEdits([ { editType: CellEditType.Metadata, index, metadata: result } - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); } catch { } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index eea2a5a0c8c..91e50defad3 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -11,7 +11,7 @@ import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { hash } from 'vs/base/common/hash'; import { toFormattedString } from 'vs/base/common/jsonFormatter'; -import { ICellOutput, IOutputDto, IOutputItemDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellOutput, INotebookTextModel, IOutputDto, IOutputItemDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel'; import { URI } from 'vs/base/common/uri'; import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; @@ -117,7 +117,7 @@ export abstract class DiffElementViewModelBase extends Disposable { private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; constructor( - readonly mainDocumentTextModel: NotebookTextModel, + readonly mainDocumentTextModel: INotebookTextModel, readonly original: DiffNestedCellViewModel | undefined, readonly modified: DiffNestedCellViewModel | undefined, readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', @@ -645,7 +645,7 @@ function outputsEqual(original: ICellOutput[], modified: ICellOutput[]) { return OutputComparison.Unchanged; } -export function getFormattedMetadataJSON(documentTextModel: NotebookTextModel, metadata: NotebookCellMetadata, language?: string) { +export function getFormattedMetadataJSON(documentTextModel: INotebookTextModel, metadata: NotebookCellMetadata, language?: string) { let filteredMetadata: { [key: string]: any } = {}; if (documentTextModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 1672b6635b2..9de089e2f5a 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -8,8 +8,8 @@ import * as DOM from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { IEditorOpenContext } from 'vs/workbench/common/editor'; -import { cellEditorBackground, getDefaultNotebookCreationOptions, notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, IEditorOpenContext, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, IEditorPaneWithSelection } from 'vs/workbench/common/editor'; +import { cellEditorBackground, focusedEditorBorderColor, getDefaultNotebookCreationOptions, notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -42,10 +42,46 @@ import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; const $ = DOM.$; -export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor, INotebookDelegateForWebview { +class NotebookDiffEditorSelection implements IEditorPaneSelection { + + constructor( + private readonly selections: number[] + ) { } + + compare(other: IEditorPaneSelection): EditorPaneSelectionCompareResult { + if (!(other instanceof NotebookDiffEditorSelection)) { + return EditorPaneSelectionCompareResult.DIFFERENT; + } + + if (this.selections.length !== other.selections.length) { + return EditorPaneSelectionCompareResult.DIFFERENT; + } + + for (let i = 0; i < this.selections.length; i++) { + if (this.selections[i] !== other.selections[i]) { + return EditorPaneSelectionCompareResult.DIFFERENT; + } + } + + return EditorPaneSelectionCompareResult.IDENTICAL; + } + + restore(options: IEditorOptions): INotebookEditorOptions { + const notebookOptions: INotebookEditorOptions = { + cellSelections: cellIndexesToRanges(this.selections) + }; + + Object.assign(notebookOptions, options); + return notebookOptions; + } +} + +export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor, INotebookDelegateForWebview, IEditorPaneWithSelection { creationOptions: INotebookEditorCreationOptions = getDefaultNotebookCreationOptions(); static readonly ID: string = NOTEBOOK_DIFF_EDITOR_ID; @@ -86,6 +122,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _layoutCancellationTokenSource?: CancellationTokenSource; + private readonly _onDidChangeSelection = this._register(new Emitter<IEditorPaneSelectionChangeEvent>()); + readonly onDidChangeSelection = this._onDidChangeSelection.event; + private _isDisposed: boolean = false; get isDisposed() { @@ -110,6 +149,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._revealFirst = true; } + getSelection(): IEditorPaneSelection | undefined { + const selections = this._list.getFocus(); + return new NotebookDiffEditorSelection(selections); + } + toggleNotebookCellSelection(cell: IGenericCellViewModel) { // throw new Error('Method not implemented.'); } @@ -227,6 +271,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } })); + this._register(this._list.onDidChangeFocus(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.USER }))); + // transparent cover this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover')); this._webviewTransparentCover.style.display = 'none'; @@ -333,7 +379,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._modifiedResourceDisposableStore.add(this._modifiedWebview); } - await this.updateLayout(this._layoutCancellationTokenSource.token); + await this.updateLayout(this._layoutCancellationTokenSource.token, options?.cellSelections ? cellRangesToIndexes(options.cellSelections) : undefined); } private _detachModel() { @@ -415,7 +461,14 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._originalWebview.element.style.left = `16px`; } - async updateLayout(token: CancellationToken) { + override setOptions(options: INotebookEditorOptions | undefined): void { + const selections = options?.cellSelections ? cellRangesToIndexes(options.cellSelections) : undefined; + if (selections) { + this._list.setFocus(selections); + } + } + + async updateLayout(token: CancellationToken, selections?: number[]) { if (!this._model) { return; } @@ -445,6 +498,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._list.setFocus([firstChangeIndex]); this._list.reveal(firstChangeIndex, 0.3); } + + if (selections) { + this._list.setFocus(selections); + } } private _isViewModelTheSame(viewModels: DiffElementViewModelBase[]) { @@ -929,6 +986,15 @@ registerThemingParticipant((theme, collector) => { }`); } + const focusCellBackgroundColor = theme.getColor(focusedEditorBorderColor); + + if (focusCellBackgroundColor) { + collector.addRule(`.notebook-text-diff-editor .monaco-list-row.focused .cell-body .border-container .top-border { border-top: 1px solid ${focusCellBackgroundColor};}`); + collector.addRule(`.notebook-text-diff-editor .monaco-list-row.focused .cell-body .border-container .bottom-border { border-top: 1px solid ${focusCellBackgroundColor};}`); + collector.addRule(`.notebook-text-diff-editor .monaco-list-row.focused .cell-body .border-container .left-border { border-left: 1px solid ${focusCellBackgroundColor};}`); + collector.addRule(`.notebook-text-diff-editor .monaco-list-row.focused .cell-body .border-container .right-border { border-right: 1px solid ${focusCellBackgroundColor};}`); + } + const diffDiagonalFillColor = theme.getColor(diffDiagonalFill); collector.addRule(` .notebook-text-diff-editor .diagonal-fill { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index b774588523b..fead08ce397 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./notebookDiff'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import * as DOM from 'vs/base/browser/dom'; -import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget'; +import { IListOptions, IListStyles, isMonacoEditor, IStyleController, MouseController } from 'vs/base/browser/ui/list/listWidget'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -281,6 +281,17 @@ export class CellDiffSideBySideRenderer implements IListRenderer<SideBySideDiffE } } +export class NotebookMouseController<T> extends MouseController<T> { + protected override onViewPointer(e: IListMouseEvent<T>): void { + if (isMonacoEditor(e.browserEvent.target as HTMLElement)) { + const focus = typeof e.index === 'undefined' ? [] : [e.index]; + this.list.setFocus(focus, e.browserEvent); + } else { + super.onViewPointer(e); + } + } +} + export class NotebookTextDiffList extends WorkbenchList<DiffElementViewModelBase> implements IDisposable, IStyleController { private styleElement?: HTMLStyleElement; @@ -302,6 +313,10 @@ export class NotebookTextDiffList extends WorkbenchList<DiffElementViewModelBase super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); } + protected override createMouseController(options: IListOptions<DiffElementViewModelBase>): MouseController<DiffElementViewModelBase> { + return new NotebookMouseController(this); + } + getAbsoluteTopOfElement(element: DiffElementViewModelBase): number { const index = this.indexOf(element); // if (index === undefined || index < 0 || index >= this.length) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 9fdabf420e3..2b793c40961 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -29,7 +29,7 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd import { isCompositeNotebookEditorInput, NotebookEditorInput, NotebookEditorInputOptions } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; -import { CellKind, CellUri, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookWorkingCopyTypeIdentifier, NotebookSetting, ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookWorkingCopyTypeIdentifier, NotebookSetting, ICellOutput, ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; @@ -99,7 +99,6 @@ import { NotebookExecutionService } from 'vs/workbench/contrib/notebook/browser/ import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookKeymapService } from 'vs/workbench/contrib/notebook/common/notebookKeymapService'; import { NotebookKeymapService } from 'vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -322,13 +321,16 @@ class CellContentProvider implements ITextModelContentProvider { } } - if (result) { - const once = Event.any(result.onWillDispose, ref.object.notebook.onWillDispose)(() => { - once.dispose(); - ref.dispose(); - }); + if (!result) { + ref.dispose(); + return null; } + const once = Event.any(result.onWillDispose, ref.object.notebook.onWillDispose)(() => { + once.dispose(); + ref.dispose(); + }); + return result; } } @@ -400,13 +402,16 @@ class CellInfoContentProvider { } } - if (result) { - const once = result.onWillDispose(() => { - once.dispose(); - ref.dispose(); - }); + if (!result) { + ref.dispose(); + return null; } + const once = result.onWillDispose(() => { + once.dispose(); + ref.dispose(); + }); + return result; } @@ -429,7 +434,7 @@ class CellInfoContentProvider { private _getResult(data: { notebook: URI; outputId?: string | undefined; - }, cell: NotebookCellTextModel) { + }, cell: ICell) { let result: { content: string; mode: ILanguageSelection } | undefined = undefined; const mode = this._languageService.createById('json'); @@ -472,34 +477,36 @@ class CellInfoContentProvider { const cell = ref.object.notebook.cells.find(cell => !!cell.outputs.find(op => op.outputId === data.outputId)); if (!cell) { + ref.dispose(); return null; } const result = this._getResult(data, cell); - if (result) { - const model = this._modelService.createModel(result.content, result.mode, resource); - const cellModelListener = Event.any(cell.onDidChangeOutputs, cell.onDidChangeOutputItems)(() => { - const newResult = this._getResult(data, cell); + if (!result) { + ref.dispose(); + return null; + } - if (!newResult) { - return; - } + const model = this._modelService.createModel(result.content, result.mode, resource); + const cellModelListener = Event.any(cell.onDidChangeOutputs ?? Event.None, cell.onDidChangeOutputItems ?? Event.None)(() => { + const newResult = this._getResult(data, cell); - model.setValue(newResult.content); - model.setMode(newResult.mode.languageId); - }); + if (!newResult) { + return; + } - const once = model.onWillDispose(() => { - once.dispose(); - cellModelListener.dispose(); - ref.dispose(); - }); + model.setValue(newResult.content); + model.setMode(newResult.mode.languageId); + }); - return model; - } + const once = model.onWillDispose(() => { + once.dispose(); + cellModelListener.dispose(); + ref.dispose(); + }); - return null; + return model; } } @@ -554,6 +561,15 @@ class NotebookEditorManager implements IWorkbenchContribution { group.closeEditors(staleInputs); } })); + + // CLOSE editors when we are about to open conflicting notebooks + this._disposables.add(_notebookEditorModelService.onWillFailWithConflict(e => { + for (const group of editorGroups.groups) { + const conflictInputs = group.editors.filter(input => input instanceof NotebookEditorInput && input.viewType !== e.viewType && isEqual(input.resource, e.resource)); + const p = group.closeEditors(conflictInputs); + e.waitUntil(p); + } + })); } dispose(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index aed8ee62af8..7153bcfe26c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -26,6 +26,7 @@ import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKe import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { cellRangesToIndexes, ICellRange, reduceCellRanges } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IWebview } from 'vs/workbench/contrib/webview/browser/webview'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; //#region Shared commands export const EXPAND_CELL_INPUT_COMMAND_ID = 'notebook.cell.expandCellInput'; @@ -628,6 +629,11 @@ export interface IActiveNotebookEditor extends INotebookEditor { getNextVisibleCellIndex(index: number): number; } +export interface IBaseCellEditorOptions extends IDisposable { + readonly value: IEditorOptions; + readonly onDidChange: Event<void>; +} + /** * A mix of public interface and internal one (used by internal rendering code, e.g., cellRenderer) */ @@ -637,6 +643,7 @@ export interface INotebookEditorDelegate extends INotebookEditor { readonly creationOptions: INotebookEditorCreationOptions; readonly onDidChangeOptions: Event<void>; readonly onDidChangeDecorations: Event<void>; + getBaseCellEditorOptions(language: string): IBaseCellEditorOptions; createMarkupPreview(cell: ICellViewModel): Promise<void>; unhideMarkupPreviews(cells: readonly ICellViewModel[]): Promise<void>; hideMarkupPreviews(cells: readonly ICellViewModel[]): Promise<void>; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 5420ef359f9..623686523fa 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -49,7 +49,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti private _rootElement!: HTMLElement; private _dimension?: DOM.Dimension; - private readonly inputListener = this._register(new MutableDisposable()); + private readonly _inputListener = this._register(new MutableDisposable()); // override onDidFocus and onDidBlur to be based on the NotebookEditorWidget element private readonly _onDidFocusWidget = this._register(new Emitter<void>()); @@ -66,36 +66,36 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorDropService private readonly _editorDropService: IEditorDropService, @INotebookEditorService private readonly _notebookWidgetService: INotebookEditorService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IFileService private readonly fileService: IFileService, + @IFileService private readonly _fileService: IFileService, @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService ) { super(NotebookEditor.ID, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento<INotebookEditorViewState>(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._onDidChangeFileSystemProvider(e.scheme))); + this._register(this._fileService.onDidChangeFileSystemProviderRegistrations(e => this._onDidChangeFileSystemProvider(e.scheme))); } - private onDidChangeFileSystemProvider(scheme: string): void { + private _onDidChangeFileSystemProvider(scheme: string): void { if (this.input instanceof NotebookEditorInput && this.input.resource?.scheme === scheme) { - this.updateReadonly(this.input); + this._updateReadonly(this.input); } } - private onDidChangeInputCapabilities(input: NotebookEditorInput): void { + private _onDidChangeInputCapabilities(input: NotebookEditorInput): void { if (this.input === input) { - this.updateReadonly(input); + this._updateReadonly(input); } } - private updateReadonly(input: NotebookEditorInput): void { + private _updateReadonly(input: NotebookEditorInput): void { if (this._widget.value) { this._widget.value.setOptions({ isReadOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } @@ -128,7 +128,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti override getActionViewItem(action: IAction): IActionViewItem | undefined { if (action.id === SELECT_KERNEL_ID) { // this is being disposed by the consumer - return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this); + return this._instantiationService.createInstance(NotebooKernelActionViewItem, action, this); } return undefined; } @@ -170,13 +170,13 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti return !!value && (DOM.isAncestor(activeElement, value.getDomNode() || DOM.isAncestor(activeElement, value.getOverflowContainerDomNode()))); } - override async setInput(input: NotebookEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { + override async setInput(input: NotebookEditorInput, options: INotebookEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken, noRetry?: boolean): Promise<void> { try { clearMarks(input.resource); mark(input.resource, 'startTime'); const group = this.group!; - this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + this._inputListener.value = input.onDidChangeCapabilities(() => this._onDidChangeInputCapabilities(input)); this._widgetDisposableStore.clear(); @@ -186,7 +186,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti this._widget.value.onWillHide(); } - this._widget = <IBorrowValue<NotebookEditorWidget>>this.instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input); + this._widget = <IBorrowValue<NotebookEditorWidget>>this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input); this._widgetDisposableStore.add(this._widget.value!.onDidChangeModel(() => this._onDidChangeModel.fire())); this._widgetDisposableStore.add(this._widget.value!.onDidChangeActiveCell(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.USER }))); @@ -205,6 +205,16 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti return undefined; } + // The widget has been taken away again. This can happen when the tab has been closed while + // loading was in progress, in particular when open the same resource as different view type. + // When this happen, retry once + if (!this._widget.value) { + if (noRetry) { + return undefined; + } + return this.setInput(input, options, context, token, true); + } + if (model === null) { throw new Error(localize('fail.noEditor', "Cannot open resource with notebook editor type '{0}', please check if you have the right extension installed and enabled.", input.viewType)); } @@ -314,7 +324,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti } override clearInput(): void { - this.inputListener.clear(); + this._inputListener.clear(); if (this._widget.value) { this._saveEditorViewState(this.input); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 4bb50abce37..aa1bcc7853f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -21,6 +21,7 @@ import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { deepClone } from 'vs/base/common/objects'; import { setTimeout0 } from 'vs/base/common/platform'; import { extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -48,7 +49,7 @@ import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeg import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BORDER, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, IActiveNotebookEditorDelegate, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { notebookDebug } from 'vs/workbench/contrib/notebook/browser/notebookLogger'; @@ -87,6 +88,100 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; const $ = DOM.$; +export class BaseCellEditorOptions extends Disposable implements IBaseCellEditorOptions { + private static fixedEditorOptions: IEditorOptions = { + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + selectOnLineNumbers: false, + lineNumbers: 'off', + lineDecorationsWidth: 0, + folding: true, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + lineNumbersMinChars: 3 + }; + + private _localDisposableStore = this._register(new DisposableStore()); + private readonly _onDidChange = this._register(new Emitter<void>()); + readonly onDidChange: Event<void> = this._onDidChange.event; + private _value: IEditorOptions; + + get value(): Readonly<IEditorOptions> { + return this._value; + } + + constructor(readonly notebookEditor: INotebookEditorDelegate, readonly notebookOptions: NotebookOptions, readonly configurationService: IConfigurationService, readonly language: string) { + super(); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { + this._recomputeOptions(); + } + })); + + this._register(notebookOptions.onDidChangeOptions(e => { + if (e.cellStatusBarVisibility || e.editorTopPadding || e.editorOptionsCustomizations) { + this._recomputeOptions(); + } + })); + + this._register(this.notebookEditor.onDidChangeModel(() => { + this._localDisposableStore.clear(); + + if (this.notebookEditor.hasModel()) { + this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { + this._recomputeOptions(); + })); + + this._recomputeOptions(); + } + })); + + if (this.notebookEditor.hasModel()) { + this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { + this._recomputeOptions(); + })); + } + + this._value = this._computeEditorOptions(); + } + + private _recomputeOptions(): void { + this._value = this._computeEditorOptions(); + this._onDidChange.fire(); + } + + private _computeEditorOptions() { + const editorOptions = deepClone(this.configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: this.language })); + const layoutConfig = this.notebookOptions.getLayoutConfiguration(); + const editorOptionsOverrideRaw = layoutConfig.editorOptionsCustomizations ?? {}; + const editorOptionsOverride: { [key: string]: any } = {}; + for (const key in editorOptionsOverrideRaw) { + if (key.indexOf('editor.') === 0) { + editorOptionsOverride[key.substring(7)] = editorOptionsOverrideRaw[key]; + } + } + const computed = Object.freeze({ + ...editorOptions, + ...BaseCellEditorOptions.fixedEditorOptions, + ...editorOptionsOverride, + ...{ padding: { top: 12, bottom: 12 } }, + readOnly: this.notebookEditor.isReadOnly + }); + + return computed; + } +} + export function getDefaultNotebookCreationOptions(): INotebookEditorCreationOptions { // We inlined the id to avoid loading comment contrib in tests const skipContributions = ['editor.contrib.review']; @@ -229,6 +324,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return this._list.visibleRanges || []; } + private _baseCellEditorOptions = new Map<string, IBaseCellEditorOptions>(); + readonly isEmbedded: boolean; private _readOnly: boolean; @@ -452,6 +549,19 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } //#region Editor Core + + getBaseCellEditorOptions(language: string): IBaseCellEditorOptions { + const existingOptions = this._baseCellEditorOptions.get(language); + + if (existingOptions) { + return existingOptions; + } else { + const options = new BaseCellEditorOptions(this, this.notebookOptions, this.configurationService, language); + this._baseCellEditorOptions.set(language, options); + return options; + } + } + private _updateForNotebookConfiguration() { if (!this._overlayContainer) { return; @@ -2809,6 +2919,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._overlayContainer.remove(); this.viewModel?.dispose(); + this._renderedEditors.clear(); + this._baseCellEditorOptions.forEach(v => v.dispose()); + this._baseCellEditorOptions.clear(); + + super.dispose(); + // unref this._webview = null; this._webviewResolvePromise = null; @@ -2817,11 +2933,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._listTopCellToolbar = null; this._notebookViewModel = undefined; this._cellContextKeyManager = null; - this._renderedEditors.clear(); + this._notebookTopToolbar = null!; + this._list = null!; + this._listViewInfoAccessor = null!; this._pendingLayouts = null; this._listDelegate = null; - - super.dispose(); } toJSON(): { notebookUri: URI | undefined } { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 1999bf283bc..374f1d707a1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -594,12 +594,10 @@ export class NotebookService extends Disposable implements INotebookService { return this._registerProviderData(viewType, new SimpleNotebookProviderInfo(viewType, serializer, extensionData)); } - async withNotebookDataProvider(resource: URI, viewType?: string): Promise<ComplexNotebookProviderInfo | SimpleNotebookProviderInfo> { - const providers = this.notebookProviderInfoStore.getContributedNotebook(resource); - // If we have a viewtype specified we want that data provider, as the resource won't always map correctly - const selected = viewType ? providers.find(p => p.id === viewType) : providers[0]; + async withNotebookDataProvider(viewType: string): Promise<ComplexNotebookProviderInfo | SimpleNotebookProviderInfo> { + const selected = this.notebookProviderInfoStore.get(viewType); if (!selected) { - throw new Error(`NO contribution for resource: '${resource.toString()}'`); + throw new Error(`UNKNOWN notebook type '${viewType}'`); } await this.canResolve(selected.id); const result = this._notebookProviders.get(selected.id); @@ -714,4 +712,3 @@ export class NotebookService extends Disposable implements INotebookService { } } - diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts index 2f7413ad92f..8c0b8a99748 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts @@ -68,7 +68,7 @@ export class CellDragAndDropController extends Disposable { private readonly listOnWillScrollListener = this._register(new MutableDisposable()); constructor( - private readonly notebookEditor: INotebookEditorDelegate, + private notebookEditor: INotebookEditorDelegate, private readonly notebookListContainer: HTMLElement ) { super(); @@ -406,6 +406,11 @@ export class CellDragAndDropController extends Disposable { return this.getDropInsertDirection(dragPosRatio); } + + override dispose() { + this.notebookEditor = null!; + super.dispose(); + } } export function performCellDropEdits(editor: INotebookEditorDelegate, draggedCell: ICellViewModel, dropDirection: 'above' | 'below', draggedOverCell: ICellViewModel): void { @@ -494,6 +499,6 @@ export function performCellDropEdits(editor: INotebookEditorDelegate, draggedCel true, { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), - undefined); + undefined, true); editor.revealCellRangeInView(finalSelection); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts index 00d68c38911..3059080c7d5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { deepClone } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { IEditorOptions, LineNumbersType } from 'vs/editor/common/config/editorOptions'; import { localize } from 'vs/nls'; @@ -17,7 +15,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { Registry } from 'vs/platform/registry/common/platform'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { INotebookCellToolbarActionContext, INotebookCommandContext, NotebookMultiCellAction, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IBaseCellEditorOptions, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellInternalMetadata, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -25,66 +23,18 @@ import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOp import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; export class CellEditorOptions extends CellPart { - private static fixedEditorOptions: IEditorOptions = { - scrollBeyondLastLine: false, - scrollbar: { - verticalScrollbarSize: 14, - horizontal: 'auto', - useShadows: true, - verticalHasArrows: false, - horizontalHasArrows: false, - alwaysConsumeMouseWheel: false - }, - renderLineHighlightOnlyWhenFocus: true, - overviewRulerLanes: 0, - selectOnLineNumbers: false, - lineNumbers: 'off', - lineDecorationsWidth: 0, - folding: true, - fixedOverflowWidgets: true, - minimap: { enabled: false }, - renderValidationDecorations: 'on', - lineNumbersMinChars: 3 - }; - - private _value: IEditorOptions; private _lineNumbers: 'on' | 'off' | 'inherit' = 'inherit'; private readonly _onDidChange = this._register(new Emitter<void>()); readonly onDidChange: Event<void> = this._onDidChange.event; - private _localDisposableStore = this._register(new DisposableStore()); + private _value: IEditorOptions; - constructor(readonly notebookEditor: INotebookEditorDelegate, readonly notebookOptions: NotebookOptions, readonly configurationService: IConfigurationService, readonly language: string) { + constructor(private readonly base: IBaseCellEditorOptions, readonly notebookOptions: NotebookOptions, readonly configurationService: IConfigurationService) { super(); - this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { - this._recomputeOptions(); - } - })); - this._register(notebookOptions.onDidChangeOptions(e => { - if (e.cellStatusBarVisibility || e.editorTopPadding || e.editorOptionsCustomizations) { - this._recomputeOptions(); - } + this._register(base.onDidChange(() => { + this._recomputeOptions(); })); - this._register(this.notebookEditor.onDidChangeModel(() => { - this._localDisposableStore.clear(); - - if (this.notebookEditor.hasModel()) { - this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { - this._recomputeOptions(); - })); - - this._recomputeOptions(); - } - })); - - if (this.notebookEditor.hasModel()) { - this._localDisposableStore.add(this.notebookEditor.onDidChangeOptions(() => { - this._recomputeOptions(); - })); - } - this._value = this._computeEditorOptions(); } @@ -102,25 +52,17 @@ export class CellEditorOptions extends CellPart { private _computeEditorOptions() { const renderLineNumbers = this.configurationService.getValue<'on' | 'off'>('notebook.lineNumbers') === 'on'; const lineNumbers: LineNumbersType = renderLineNumbers ? 'on' : 'off'; - const editorOptions = deepClone(this.configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: this.language })); - const layoutConfig = this.notebookOptions.getLayoutConfiguration(); - const editorOptionsOverrideRaw = layoutConfig.editorOptionsCustomizations ?? {}; - const editorOptionsOverride: { [key: string]: any } = {}; - for (const key in editorOptionsOverrideRaw) { - if (key.indexOf('editor.') === 0) { - editorOptionsOverride[key.substring(7)] = editorOptionsOverrideRaw[key]; - } - } - const computed = { - ...editorOptions, - ...CellEditorOptions.fixedEditorOptions, - ... { lineNumbers }, - ...editorOptionsOverride, - ...{ padding: { top: 12, bottom: 12 } }, - readOnly: this.notebookEditor.isReadOnly - }; - return computed; + const value = this.base.value; + + if (value.lineNumbers !== lineNumbers) { + return { + ...value, + ...{ lineNumbers } + }; + } else { + return Object.assign({}, value); + } } getUpdatedValue(internalMetadata: NotebookCellInternalMetadata, cellUri: URI): IEditorOptions { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index fab7ccb3505..3f2df96e305 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -53,8 +53,8 @@ export class CodeCell extends Disposable { ) { super(); - const cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor, this.notebookEditor.notebookOptions, this.configurationService, this.viewCell.language)); - this._outputContainerRenderer = this.instantiationService.createInstance(CellOutputContainer, notebookEditor, viewCell, templateData, { limit: 2 }); + const cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(viewCell.language), this.notebookEditor.notebookOptions, this.configurationService)); + this._outputContainerRenderer = this.instantiationService.createInstance(CellOutputContainer, notebookEditor, viewCell, templateData, { limit: 500 }); this.cellParts = [...templateData.cellParts, cellEditorOptions, this._outputContainerRenderer]; const editorHeight = this.calculateInitEditorHeight(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts index b1b5077afea..bf119feb15c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts @@ -59,8 +59,7 @@ export class StatefulMarkdownCell extends Disposable { this.constructDOM(); this.editorPart = templateData.editorPart; - - this.cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor, this.notebookEditor.notebookOptions, this.configurationService, this.viewCell.language)); + this.cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(viewCell.language), this.notebookEditor.notebookOptions, this.configurationService)); this.cellEditorOptions.setLineNumbers(this.viewCell.lineNumbers); this.editorOptions = this.cellEditorOptions.getValue(this.viewCell.internalMetadata, this.viewCell.uri); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 673fe4a4312..3ef1c58649e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -1252,8 +1252,20 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID this.view.setScrollTop(this.view.elementTop(viewIndex)); break; case CellRevealPosition.Center: - this.view.setScrollTop(elementTop - this.view.renderHeight / 2); - this.view.setScrollTop(this.view.elementTop(viewIndex) - this.view.renderHeight / 2); + { + // reveal the cell top in the viewport center initially + this.view.setScrollTop(elementTop - this.view.renderHeight / 2); + // cell rendered already, we now have a more accurate cell height + const newElementTop = this.view.elementTop(viewIndex); + const newElementHeight = this.view.elementHeight(viewIndex); + const renderHeight = this.getViewScrollBottom() - this.getViewScrollTop(); + if (newElementHeight >= renderHeight) { + // cell is larger than viewport, reveal top + this.view.setScrollTop(newElementTop); + } else { + this.view.setScrollTop(newElementTop + (newElementHeight / 2) - (renderHeight / 2)); + } + } break; case CellRevealPosition.Bottom: this.view.setScrollTop(this.scrollTop + (elementBottom - wrapperBottom)); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 1beeb548519..02beef093cf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -98,7 +98,7 @@ abstract class AbstractCellRenderer { language: string, protected dndController: CellDragAndDropController | undefined ) { - this.editorOptions = new CellEditorOptions(notebookEditor, notebookEditor.notebookOptions, configurationService, language); + this.editorOptions = new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(language), this.notebookEditor.notebookOptions, configurationService); } dispose() { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 11b51e17e2f..8b7c306af28 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -156,6 +156,8 @@ export abstract class BaseCellViewModel extends Disposable { this._onDidChangeState.fire({ outputCollapsedChanged: true }); } + private _textEditorRestore: any; + constructor( readonly viewType: string, readonly model: NotebookCellTextModel, @@ -234,7 +236,9 @@ export abstract class BaseCellViewModel extends Disposable { this._textEditor = editor; if (this._editorViewStates) { - this._restoreViewState(this._editorViewStates); + this._textEditorRestore = setTimeout(() => { + this._restoreViewState(this._editorViewStates); + }); } if (this._editorTransientState) { @@ -260,6 +264,7 @@ export abstract class BaseCellViewModel extends Disposable { } detachTextEditor() { + clearTimeout(this._textEditorRestore); this.saveViewState(); this.saveTransientState(); // decorations need to be cleared first as editors can be resued. @@ -584,6 +589,7 @@ export abstract class BaseCellViewModel extends Disposable { super.dispose(); dispose(this._editorListeners); + clearTimeout(this._textEditorRestore); // Only remove the undo redo stack if we map this cell uri to itself // If we are not in perCell mode, it will map to the full NotebookDocument and diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index f897a762e74..243b4b96edc 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -602,6 +602,17 @@ export class NotebookEditorToolbar extends Disposable { } this._computeSizes(); } + + override dispose() { + this._notebookLeftToolbar.context = undefined; + this._notebookRightToolbar.context = undefined; + this._notebookLeftToolbar.dispose(); + this._notebookRightToolbar.dispose(); + this._notebookLeftToolbar = null!; + this._notebookRightToolbar = null!; + + super.dispose(); + } } registerThemingParticipant((theme, collector) => { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index a04f9eb9ecf..5f186e4b0da 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten } from 'vs/base/common/arrays'; import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -324,7 +323,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel case 'language': this._pauseableEmitter.fire({ - rawEvents: [{ kind: NotebookCellsChangeType.ChangeLanguage, index: this._getCellIndexByHandle(cell.handle), language: cell.language, transient: false }], + rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this._getCellIndexByHandle(cell.handle), language: cell.language, transient: false }], versionId: this.versionId, synchronous: true, endSelectionState: undefined @@ -400,11 +399,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel ], true, undefined, () => undefined, - undefined + undefined, + true ); } - applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean = true): boolean { + applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean): boolean { this._pauseableEmitter.pause(); this.pushStackElement('edit', beginSelectionState, undoRedoGroup); @@ -502,7 +502,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return [...otherEdits.reverse(), ...replaceEdits]; }); - const flattenEdits = flatten(edits); + const flattenEdits = edits.flat(); for (const { edit, cellIndex } of flattenEdits) { switch (edit.editType) { @@ -937,7 +937,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } this._pauseableEmitter.fire({ - rawEvents: [{ kind: NotebookCellsChangeType.ChangeLanguage, index: this._cells.indexOf(cell), language: languageId, transient: false }], + rawEvents: [{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this._cells.indexOf(cell), language: languageId, transient: false }], versionId: this.versionId, synchronous: true, endSelectionState: undefined diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index c5ffb1bbf2f..bc1f504dc36 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -5,7 +5,7 @@ import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDiffResult, ISequence } from 'vs/base/common/diff/diff'; +import { IDiffResult } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; @@ -18,11 +18,13 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { Command } from 'vs/editor/common/languages'; +import { IReadonlyTextBuffer } from 'vs/editor/common/model'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -214,7 +216,10 @@ export interface ICell { outputs: ICellOutput[]; metadata: NotebookCellMetadata; internalMetadata: NotebookCellInternalMetadata; + getHashValue(): number; + textBuffer: IReadonlyTextBuffer; onDidChangeOutputs?: Event<NotebookCellOutputsSplice>; + onDidChangeOutputItems?: Event<void>; onDidChangeLanguage: Event<string>; onDidChangeMetadata: Event<void>; onDidChangeInternalMetadata: Event<CellInternalMetadataChangedEvent>; @@ -223,10 +228,14 @@ export interface ICell { export interface INotebookTextModel { readonly viewType: string; metadata: NotebookDocumentMetadata; + readonly transientOptions: TransientOptions; readonly uri: URI; readonly versionId: number; - + readonly length: number; readonly cells: readonly ICell[]; + reset(cells: ICellDto2[], metadata: NotebookDocumentMetadata, transientOptions: TransientOptions): void; + applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo?: boolean): boolean; + onDidChangeContent: Event<NotebookTextModelChangedEvent>; onWillDispose: Event<void>; } @@ -257,7 +266,7 @@ export interface IMainCellDto { export enum NotebookCellsChangeType { ModelChange = 1, Move = 2, - ChangeLanguage = 5, + ChangeCellLanguage = 5, Initialize = 6, ChangeCellMetadata = 7, Output = 8, @@ -308,7 +317,7 @@ export interface NotebookOutputItemChangedEvent { } export interface NotebookCellsChangeLanguageEvent { - readonly kind: NotebookCellsChangeType.ChangeLanguage; + readonly kind: NotebookCellsChangeType.ChangeCellLanguage; readonly index: number; readonly language: string; } @@ -775,7 +784,7 @@ export interface INotebookEditorModel extends IEditorModel { readonly onDidChangeReadonly: Event<void>; readonly resource: URI; readonly viewType: string; - readonly notebook: NotebookTextModel | undefined; + readonly notebook: INotebookTextModel | undefined; isResolved(): this is IResolvedNotebookEditorModel; isDirty(): boolean; isReadonly(): boolean; @@ -869,20 +878,6 @@ export interface INotebookCellStatusBarItemProvider { provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise<INotebookCellStatusBarItemList | undefined>; } -export class CellSequence implements ISequence { - - constructor(readonly textModel: NotebookTextModel) { - } - - getElements(): string[] | number[] | Int32Array { - const hashValue = new Int32Array(this.textModel.cells.length); - for (let i = 0; i < this.textModel.cells.length; i++) { - hashValue[i] = this.textModel.cells[i].getHashValue(); - } - - return hashValue; - } -} export interface INotebookDiffResult { cellsDiff: IDiffResult; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 31ed15060f2..cffd2b0362d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -262,7 +262,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } if (this.options._backupId) { - const info = await this._notebookService.withNotebookDataProvider(this._editorModelReference.object.notebook.uri, this._editorModelReference.object.notebook.viewType); + const info = await this._notebookService.withNotebookDataProvider(this._editorModelReference.object.notebook.viewType); if (!(info instanceof SimpleNotebookProviderInfo)) { throw new Error('CANNOT open file notebook with this provider'); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 9e3e00d91ee..8938cff2e80 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -665,7 +665,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise<NotebookFileWorkingCopyModel> { - const info = await this._notebookService.withNotebookDataProvider(resource, this._viewType); + const info = await this._notebookService.withNotebookDataProvider(this._viewType); if (!(info instanceof SimpleNotebookProviderInfo)) { throw new Error('CANNOT open file notebook with this provider'); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index b2039e2694b..e2d2a6c4fe4 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -7,10 +7,21 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { URI } from 'vs/base/common/uri'; import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IReference } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; +import { Event, IWaitUntil } from 'vs/base/common/event'; export const INotebookEditorModelResolverService = createDecorator<INotebookEditorModelResolverService>('INotebookModelResolverService'); +/** + * A notebook file can only be opened ONCE per notebook type. + * This event fires when a file is already open as type A + * and there is request to open it as type B. Listeners must + * do cleanup (close editor, release references) or the request fails + */ +export interface INotebookConflictEvent extends IWaitUntil { + resource: URI; + viewType: string; +} + export interface IUntitledNotebookResource { /** * Depending on the value of `untitledResource` will @@ -34,6 +45,8 @@ export interface INotebookEditorModelResolverService { readonly onDidSaveNotebook: Event<URI>; readonly onDidChangeDirty: Event<IResolvedNotebookEditorModel>; + readonly onWillFailWithConflict: Event<INotebookConflictEvent>; + isDirty(resource: URI): boolean; resolve(resource: URI, viewType?: string): Promise<IReference<IResolvedNotebookEditorModel>>; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index f5d98feee26..bc34431e3f5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -10,15 +10,16 @@ import { ComplexNotebookEditorModel, NotebookFileWorkingCopyModel, NotebookFileW import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { ComplexNotebookProviderInfo, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ILogService } from 'vs/platform/log/common/log'; -import { Emitter, Event } from 'vs/base/common/event'; +import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { INotebookEditorModelResolverService, IUntitledNotebookResource } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { INotebookConflictEvent, INotebookEditorModelResolverService, IUntitledNotebookResource } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceMap } from 'vs/base/common/map'; import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { Schemas } from 'vs/base/common/network'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { assertIsDefined } from 'vs/base/common/types'; +import { CancellationToken } from 'vs/base/common/cancellation'; class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IResolvedNotebookEditorModel>> { @@ -61,7 +62,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IReso protected async createReferencedObject(key: string, viewType: string, hasAssociatedFilePath: boolean): Promise<IResolvedNotebookEditorModel> { const uri = URI.parse(key); - const info = await this._notebookService.withNotebookDataProvider(uri, viewType); + const info = await this._notebookService.withNotebookDataProvider(viewType); let result: IResolvedNotebookEditorModel; @@ -136,6 +137,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes readonly onDidSaveNotebook: Event<URI>; readonly onDidChangeDirty: Event<IResolvedNotebookEditorModel>; + private readonly _onWillFailWithConflict = new AsyncEmitter<INotebookConflictEvent>(); + readonly onWillFailWithConflict = this._onWillFailWithConflict.event; + constructor( @IInstantiationService instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @@ -208,7 +212,14 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes } if (existingViewType && existingViewType !== viewType) { - throw new Error(`A notebook with view type '${existingViewType}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`); + + await this._onWillFailWithConflict.fireAsync({ resource, viewType }, CancellationToken.None); + + // check again, listener should have done cleanup + const existingViewType2 = this._notebookService.getNotebookTextModel(resource)?.viewType; + if (existingViewType2 && existingViewType2 !== viewType) { + throw new Error(`A notebook with view type '${existingViewType2}' already exists for '${resource}', CANNOT create another notebook with view type ${viewType}`); + } } const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath); diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 305f67e5cbb..18faf5fdee5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -68,7 +68,7 @@ export interface INotebookService { registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: INotebookContentProvider): IDisposable; registerNotebookSerializer(viewType: string, extensionData: NotebookExtensionDescription, serializer: INotebookSerializer): IDisposable; - withNotebookDataProvider(resource: URI, viewType?: string): Promise<ComplexNotebookProviderInfo | SimpleNotebookProviderInfo>; + withNotebookDataProvider(viewType: string): Promise<ComplexNotebookProviderInfo | SimpleNotebookProviderInfo>; getOutputMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined, output: IOutputDto): readonly IOrderedMimeType[]; diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts index ace5f4f4c15..51b61d425b1 100644 --- a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts +++ b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts @@ -122,7 +122,7 @@ class MirrorNotebookDocument { } else if (e.kind === NotebookCellsChangeType.Output) { const cell = this.cells[e.index]; cell.outputs = e.outputs; - } else if (e.kind === NotebookCellsChangeType.ChangeLanguage) { + } else if (e.kind === NotebookCellsChangeType.ChangeCellLanguage) { const cell = this.cells[e.index]; cell.language = e.language; } else if (e.kind === NotebookCellsChangeType.ChangeCellMetadata) { diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts index 5a469da5487..08fd7b6e023 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellOperations.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; -import { changeCellToKind, computeCellLinesContents, copyCellRange, joinNotebookCells, moveCellRange, moveCellToIdx, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; +import { changeCellToKind, computeCellLinesContents, copyCellRange, joinNotebookCells, moveCellRange, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CellEditType, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Range } from 'vs/editor/common/core/range'; @@ -13,56 +13,6 @@ import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; suite('CellOperations', () => { - test('move cells down', async function () { - await withTestNotebook( - [ - ['//a', 'javascript', CellKind.Code, [], {}], - ['//b', 'javascript', CellKind.Code, [], {}], - ['//c', 'javascript', CellKind.Code, [], {}], - ], - (editor, viewModel) => { - moveCellToIdx(editor, 0, 1, 0, true); - // no-op - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//b'); - - moveCellToIdx(editor, 0, 1, 1, true); - // b, a, c - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//c'); - - moveCellToIdx(editor, 0, 1, 2, true); - // a, c, b - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//a'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//c'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//b'); - } - ); - }); - - test('move cells up', async function () { - await withTestNotebook( - [ - ['//a', 'javascript', CellKind.Code, [], {}], - ['//b', 'javascript', CellKind.Code, [], {}], - ['//c', 'javascript', CellKind.Code, [], {}], - ], - (editor, viewModel) => { - moveCellToIdx(editor, 1, 1, 0, true); - // b, a, c - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//a'); - - moveCellToIdx(editor, 2, 1, 0, true); - // c, b, a - assert.strictEqual(viewModel.cellAt(0)?.getText(), '//c'); - assert.strictEqual(viewModel.cellAt(1)?.getText(), '//b'); - assert.strictEqual(viewModel.cellAt(2)?.getText(), '//a'); - } - ); - }); - test('Move cells - single cell', async function () { await withTestNotebook( [ diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts index 3a543fd6a4a..0e898675d75 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts @@ -113,7 +113,7 @@ suite('Notebook Outline', function () { ], outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); - assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '!@#$\n Überschrïft'); + assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '!@#$'); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index 2b59cccb488..8cc256883a7 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -5,14 +5,30 @@ import * as assert from 'assert'; import { VSBuffer } from 'vs/base/common/buffer'; -import { LcsDiff } from 'vs/base/common/diff/diff'; +import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; import { Mimes } from 'vs/base/common/mime'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; -import { CellKind, CellSequence } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebookDiffModel } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +class CellSequence implements ISequence { + + constructor(readonly textModel: INotebookTextModel) { + } + + getElements(): string[] | number[] | Int32Array { + const hashValue = new Int32Array(this.textModel.cells.length); + for (let i = 0; i < this.textModel.cells.length; i++) { + hashValue[i] = this.textModel.cells[i].getHashValue(); + } + + return hashValue; + } +} + + suite('NotebookCommon', () => { const configurationService = new TestConfigurationService(); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts index aebc72563ab..6f65268830f 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -68,7 +68,7 @@ suite('NotebookExecutionService', () => { async (viewModel) => { const executionService = instantiationService.createInstance(NotebookExecutionService); - const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); await assertThrowsAsync(async () => await executionService.executeNotebookCell(cell)); }); }); @@ -80,7 +80,7 @@ suite('NotebookExecutionService', () => { kernelService.registerKernel(new TestNotebookKernel({ languages: ['testlang'] })); const executionService = instantiationService.createInstance(NotebookExecutionService); - const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 1, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); await assertThrowsAsync(async () => await executionService.executeNotebookCell(cell)); }); @@ -96,7 +96,7 @@ suite('NotebookExecutionService', () => { const executeSpy = sinon.spy(); kernel.executeNotebookCellsRequest = executeSpy; - const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); await executionService.executeNotebookCells(viewModel.notebookDocument, [cell]); assert.strictEqual(executeSpy.calledOnce, true); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts index 42a2b1c6d2b..0bee94ca7a1 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts @@ -40,7 +40,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(textModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 6); @@ -63,7 +63,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 6); @@ -86,7 +86,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 3, count: 1, cells: [] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); assert.strictEqual(textModel.cells[1].getValue(), 'var c = 3;'); @@ -107,7 +107,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 4); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); @@ -129,7 +129,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 4); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); @@ -151,7 +151,7 @@ suite('NotebookTextModel', () => { const textModel = editor.textModel; textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 4); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); @@ -175,7 +175,7 @@ suite('NotebookTextModel', () => { index: Number.MAX_VALUE, editType: CellEditType.Output, outputs: [] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); }); // invalid index 2 @@ -184,7 +184,7 @@ suite('NotebookTextModel', () => { index: -1, editType: CellEditType.Output, outputs: [] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); }); textModel.applyEdits([{ @@ -194,7 +194,7 @@ suite('NotebookTextModel', () => { outputId: 'someId', outputs: [{ mime: Mimes.markdown, data: valueBytesFromString('_Hello_') }] }] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 1); @@ -208,7 +208,7 @@ suite('NotebookTextModel', () => { outputId: 'someId2', outputs: [{ mime: Mimes.markdown, data: valueBytesFromString('_Hello2_') }] }] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 2); @@ -224,7 +224,7 @@ suite('NotebookTextModel', () => { outputId: 'someId3', outputs: [{ mime: Mimes.text, data: valueBytesFromString('Last, replaced output') }] }] - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 1); @@ -262,7 +262,7 @@ suite('NotebookTextModel', () => { outputs: [{ mime: Mimes.markdown, data: valueBytesFromString('append 2') }] }] } - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 2); @@ -299,7 +299,7 @@ suite('NotebookTextModel', () => { mime: Mimes.markdown, data: valueBytesFromString('append 2') }] } - ], true, undefined, () => undefined, undefined); + ], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].outputs.length, 1, 'has 1 output'); @@ -324,7 +324,7 @@ suite('NotebookTextModel', () => { index: Number.MAX_VALUE, editType: CellEditType.Metadata, metadata: {} - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); }); // invalid index 2 @@ -333,20 +333,20 @@ suite('NotebookTextModel', () => { index: -1, editType: CellEditType.Metadata, metadata: {} - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); }); textModel.applyEdits([{ index: 0, editType: CellEditType.Metadata, metadata: { customProperty: 15 }, - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); textModel.applyEdits([{ index: 0, editType: CellEditType.Metadata, metadata: {}, - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].metadata.customProperty, undefined); @@ -366,13 +366,13 @@ suite('NotebookTextModel', () => { index: 0, editType: CellEditType.PartialMetadata, metadata: { customProperty: 15 }, - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); textModel.applyEdits([{ index: 0, editType: CellEditType.PartialMetadata, metadata: {}, - }], true, undefined, () => undefined, undefined); + }], true, undefined, () => undefined, undefined, true); assert.strictEqual(textModel.cells.length, 1); assert.strictEqual(textModel.cells[0].metadata.customProperty, 15); @@ -403,7 +403,7 @@ suite('NotebookTextModel', () => { textModel.applyEdits([ { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(textModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)] }, - ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined); + ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined, true); assert.strictEqual(textModel.cells.length, 4); assert.strictEqual(textModel.cells[0].getValue(), 'var a = 1;'); @@ -449,7 +449,7 @@ suite('NotebookTextModel', () => { editType: CellEditType.Metadata, metadata: {}, } - ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined); + ], true, undefined, () => ({ kind: SelectionStateType.Index, focus: { start: 0, end: 1 }, selections: [{ start: 0, end: 1 }] }), undefined, true); assert.notStrictEqual(changeEvent, undefined); assert.strictEqual(changeEvent!.rawEvents.length, 2); @@ -641,7 +641,7 @@ suite('NotebookTextModel', () => { } ]; - editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined); + editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined, true); assert.strictEqual(notebook.cells[0].outputs.length, 1); assert.strictEqual(notebook.cells[0].outputs[0].outputs.length, 2); @@ -671,7 +671,7 @@ suite('NotebookTextModel', () => { } ]; - editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined); + editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined, true); assert.strictEqual(notebook.cells.length, 2); assert.strictEqual(notebook.cells[0].outputs.length, 0); @@ -707,7 +707,7 @@ suite('NotebookTextModel', () => { } ]; - editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined); + editor.textModel.applyEdits(edits, true, undefined, () => undefined, undefined, true); assert.strictEqual(notebook.cells.length, 2); assert.strictEqual(notebook.cells[0].outputs.length, 1); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index ebc4e1f9cc6..becb17a1256 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -91,13 +91,13 @@ suite('NotebookViewModel', () => { const lastViewCell = viewModel.cellAt(viewModel.length - 1)!; const insertIndex = viewModel.getCellIndex(firstViewCell) + 1; - const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true); + const cell = insertCellAtIndex(viewModel, insertIndex, 'var c = 3;', 'javascript', CellKind.Code, {}, [], true, true); const addedCellIndex = viewModel.getCellIndex(cell); runDeleteAction(editor, viewModel.cellAt(addedCellIndex)!); const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1; - const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true); + const cell2 = insertCellAtIndex(viewModel, secondInsertIndex, 'var d = 4;', 'javascript', CellKind.Code, {}, [], true, true); assert.strictEqual(viewModel.length, 3); assert.strictEqual(viewModel.notebookDocument.cells.length, 3); @@ -150,7 +150,7 @@ suite('NotebookViewModel Decorations', () => { end: 2, }); - insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 0, 'var d = 6;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 2, @@ -164,7 +164,7 @@ suite('NotebookViewModel Decorations', () => { end: 2 }); - insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 3, 'var d = 7;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, @@ -207,14 +207,14 @@ suite('NotebookViewModel Decorations', () => { end: 3 }); - insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 5, 'var d = 9;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 3 }); - insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true); + insertCellAtIndex(viewModel, 4, 'var d = 10;', 'javascript', CellKind.Code, {}, [], true, true); assert.deepStrictEqual(viewModel.getTrackedRange(trackedId!), { start: 1, diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index fd0d94ee157..514c5231443 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -93,7 +93,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi return this._notebook.uri; } - get notebook() { + get notebook(): NotebookTextModel { return this._notebook; } diff --git a/src/vs/workbench/contrib/output/browser/media/output.css b/src/vs/workbench/contrib/output/browser/media/output.css index f80049ed1dc..ea0ee3e8d6b 100644 --- a/src/vs/workbench/contrib/output/browser/media/output.css +++ b/src/vs/workbench/contrib/output/browser/media/output.css @@ -30,9 +30,7 @@ } .monaco-workbench .part.sidebar > .title > .title-actions .switch-output > .monaco-select-box { - border: none !important; display: block !important; - background-color: unset !important; } .monaco-pane-view .pane > .pane-header .monaco-action-bar .switch-output.action-item.select-container { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index b49b69c4acd..77942bbbf0a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -152,7 +152,7 @@ export class SettingsEditor2 extends EditorPane { private controlsElement!: HTMLElement; private settingsTargetsWidget!: SettingsTargetsWidget; - private splitView!: SplitView; + private splitView!: SplitView<number>; private settingsTreeContainer!: HTMLElement; private settingsTree!: SettingsTree; @@ -712,9 +712,9 @@ export class SettingsEditor2 extends EditorPane { element: this.tocTreeContainer, minimumSize: SettingsEditor2.TOC_MIN_WIDTH, maximumSize: Number.POSITIVE_INFINITY, - layout: (width) => { + layout: (width, _, height) => { this.tocTreeContainer.style.width = `${width}px`; - this.tocTree.layout(undefined, width); + this.tocTree.layout(height, width); } }, startingWidth, undefined, true); this.splitView.addView({ @@ -722,9 +722,9 @@ export class SettingsEditor2 extends EditorPane { element: this.settingsTreeContainer, minimumSize: SettingsEditor2.EDITOR_MIN_WIDTH, maximumSize: Number.POSITIVE_INFINITY, - layout: (width) => { + layout: (width, _, height) => { this.settingsTreeContainer.style.width = `${width}px`; - this.settingsTree.layout(undefined, width); + this.settingsTree.layout(height, width); } }, Sizing.Distribute, undefined, true); this._register(this.splitView.onDidSashReset(() => { @@ -1008,6 +1008,25 @@ export class SettingsEditor2 extends EditorPane { } private reportModifiedSetting(props: { key: string; query: string; searchResults: ISearchResult[] | null; rawResults: ISearchResult[] | null; showConfiguredOnly: boolean; isReset: boolean; settingsTarget: SettingsTarget }): void { + type SettingsEditorModifiedSettingEvent = { + key: string; + groupId: string | undefined; + nlpIndex: number | undefined; + displayIndex: number | undefined; + showConfiguredOnly: boolean; + isReset: boolean; + target: string; + }; + type SettingsEditorModifiedSettingClassification = { + key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'The setting that is being modified.' }; + groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' }; + nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; comment: 'The index of the setting in the remote search provider results, if applicable.' }; + displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; comment: 'The index of the setting in the combined search results, if applicable.' }; + showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'owner': 'rzhao271'; comment: 'Whether the user is in the modified view, which shows configured settings only.' }; + isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'Identifies whether a setting was reset to its default value.' }; + target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'The scope of the setting, such as user or workspace.' }; + }; + this.pendingSettingUpdate = null; let groupId: string | undefined = undefined; @@ -1050,18 +1069,7 @@ export class SettingsEditor2 extends EditorPane { target: reportedTarget }; - /* __GDPR__ - "settingsEditor.settingModified" : { - "key" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "The setting that is being modified." }, - "groupId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Whether the setting is from the local search or remote search provider, if applicable." }, - "nlpIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The index of the setting in the remote search provider results, if applicable." }, - "displayIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The index of the setting in the combined search results, if applicable." }, - "showConfiguredOnly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Whether the user is in the modified view, which shows configured settings only." }, - "isReset" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Identifies whether a setting was reset to its default value." }, - "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "The scope of the setting, such as user or workspace." } - } - */ - this.telemetryService.publicLog('settingsEditor.settingModified', data); + this.telemetryService.publicLog2<SettingsEditorModifiedSettingEvent, SettingsEditorModifiedSettingClassification>('settingsEditor.settingModified', data); } private onSearchModeToggled(): void { @@ -1271,7 +1279,7 @@ export class SettingsEditor2 extends EditorPane { await this.triggerSearch(query.replace(/\u203A/g, ' ')); if (query && this.searchResultModel) { - this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel!.getUniqueResults())); + this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(this.searchResultModel!.getUniqueResults())); } } @@ -1360,7 +1368,20 @@ export class SettingsEditor2 extends EditorPane { return filterModel; } - private reportFilteringUsed(query: string, results: ISearchResult[]): void { + private reportFilteringUsed(results: ISearchResult[]): void { + type SettingsEditorFilterEvent = { + 'durations.nlpResult': number | undefined; + 'counts.nlpResult': number | undefined; + 'counts.filterResult': number | undefined; + 'requestCount': number | undefined; + }; + type SettingsEditorFilterClassification = { + 'durations.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'How long the remote search provider took, if applicable.' }; + 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; + 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of matches found by the local search provider, if applicable.' }; + 'requestCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of requests sent to Bing, if applicable.' }; + }; + const nlpResult = results[SearchResultIdx.Remote]; const nlpMetadata = nlpResult?.metadata; @@ -1382,20 +1403,13 @@ export class SettingsEditor2 extends EditorPane { const requestCount = nlpMetadata?.requestCount; const data = { - durations: duration, - counts, + 'durations.nlpResult': duration.nlpResult, + 'counts.nlpResult': counts['nlpResult'], + 'counts.filterResult': counts['filterResult'], requestCount }; - /* __GDPR__ - "settingsEditor.filter" : { - "durations.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "How long the remote search provider took, if applicable." }, - "counts.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of matches found by the remote search provider, if applicable." }, - "counts.filterResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of matches found by the local search provider, if applicable." }, - "requestCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of requests sent to Bing, if applicable." } - } - */ - this.telemetryService.publicLog('settingsEditor.filter', data); + this.telemetryService.publicLog2<SettingsEditorFilterEvent, SettingsEditorFilterClassification>('settingsEditor.filter', data); } private triggerFilterPreferences(query: string): Promise<void> { @@ -1520,15 +1534,17 @@ export class SettingsEditor2 extends EditorPane { if (isCancellationError(err)) { return Promise.reject(err); } else { - /* __GDPR__ - "settingsEditor.searchError" : { - "message": { "classification": "CallstackOrException", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "The error message of the search error." } - } - */ + type SettingsSearchErrorEvent = { + 'message': string; + }; + type SettingsSearchErrorClassification = { + 'message': { 'classification': 'CallstackOrException'; 'purpose': 'FeatureInsight'; 'owner': 'rzhao271'; 'comment': 'The error message of the search error.' }; + }; + const message = getErrorMessage(err).trim(); if (message && message !== 'Error') { // "Error" = any generic network error - this.telemetryService.publicLogError('settingsEditor.searchError', { message }); + this.telemetryService.publicLogError2<SettingsSearchErrorEvent, SettingsSearchErrorClassification>('settingsEditor.searchError', { message }); this.logService.info('Setting search error: ' + message); } return null; @@ -1545,7 +1561,7 @@ export class SettingsEditor2 extends EditorPane { // space it has, otherwise setViewVisible results in the first panel // showing up at the minimum size whenever the Settings editor // opens for the first time. - this.splitView.layout(this.bodyContainer.clientWidth); + this.splitView.layout(this.bodyContainer.clientWidth, listHeight); const firstViewWasVisible = this.splitView.isViewVisible(0); const firstViewVisible = this.bodyContainer.clientWidth >= SettingsEditor2.NARROW_TOTAL_WIDTH; diff --git a/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css index 243fc58a8a5..ef32cf43f39 100644 --- a/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css +++ b/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css @@ -36,25 +36,3 @@ .remote-help-content .monaco-list .monaco-list-row .monaco-tl-twistie { width: 0px !important; } - -.monaco-workbench .part > .title > .title-actions .switch-remote { - display: flex; - align-items: center; - font-size: 11px; - margin-right: 0.3em; - height: 20px; - flex-shrink: 1; -} - -.monaco-workbench.mac .part > .title > .title-actions .switch-remote { - border-radius: 4px; -} - -.switch-remote > .monaco-select-box { - border: none; - display: block; -} - -.monaco-workbench .part > .title > .title-actions .switch-remote > .monaco-select-box { - padding: 1px 22px 2px 6px; -} diff --git a/src/vs/workbench/contrib/remote/browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/browser/remote.contribution.ts index 6982a033c25..63e7f2df0b4 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.contribution.ts @@ -17,7 +17,7 @@ workbenchContributionsRegistry.registerWorkbenchContribution(ShowCandidateContri workbenchContributionsRegistry.registerWorkbenchContribution(TunnelFactoryContribution, LifecyclePhase.Ready); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteStatusIndicator, LifecyclePhase.Starting); -workbenchContributionsRegistry.registerWorkbenchContribution(ForwardedPortsView, LifecyclePhase.Eventually); +workbenchContributionsRegistry.registerWorkbenchContribution(ForwardedPortsView, LifecyclePhase.Restored); workbenchContributionsRegistry.registerWorkbenchContribution(PortRestore, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(AutomaticPortForwarding, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteMarkers, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index e6b3c873d57..fae968c9cd6 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -101,7 +101,8 @@ export class TunnelViewModel implements ITunnelViewModel { id: TunnelPrivacyId.Private, themeIcon: privatePortIcon.id, label: nls.localize('tunnelPrivacy.private', "Private") - } + }, + strip: () => undefined }; constructor( @@ -415,6 +416,31 @@ class ActionBarRenderer extends Disposable implements ITableRenderer<ActionBarCe })); } + private tunnelContext(tunnel: ITunnelItem): ITunnelItem { + let context: ITunnelItem | undefined; + if (tunnel instanceof TunnelItem) { + context = tunnel.strip(); + } + if (!context) { + context = { + tunnelType: tunnel.tunnelType, + remoteHost: tunnel.remoteHost, + remotePort: tunnel.remotePort, + localAddress: tunnel.localAddress, + protocol: tunnel.protocol, + localUri: tunnel.localUri, + localPort: tunnel.localPort, + name: tunnel.name, + closeable: tunnel.closeable, + source: tunnel.source, + privacy: tunnel.privacy, + processDescription: tunnel.processDescription, + label: tunnel.label + }; + } + return context; + } + renderActionBarItem(element: ActionBarCell, templateData: IActionBarTemplateData): void { templateData.label.element.style.display = 'flex'; templateData.label.setLabel(element.label, undefined, @@ -424,7 +450,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer<ActionBarCe : element.tooltip, extraClasses: element.menuId === MenuId.TunnelLocalAddressInline ? ['ports-view-actionbar-cell-localaddress'] : undefined }); - templateData.actionBar.context = element.tunnel; + templateData.actionBar.context = this.tunnelContext(element.tunnel); templateData.container.style.paddingLeft = '10px'; const context: [string, any][] = [ @@ -566,6 +592,29 @@ class TunnelItem implements ITunnelItem { tunnelService); } + /** + * Removes all non-serializable properties from the tunnel + * @returns A new TunnelItem without any services + */ + public strip(): TunnelItem | undefined { + return new TunnelItem( + this.tunnelType, + this.remoteHost, + this.remotePort, + this.source, + this.hasRunningProcess, + this.protocol, + this.localUri, + this.localAddress, + this.localPort, + this.closeable, + this.name, + this.runningProcess, + this.pid, + this._privacy + ); + } + constructor( public tunnelType: TunnelType, public remoteHost: string, diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 055172e9e9b..600b2af4b21 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -1371,7 +1371,8 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor private setViewState(state: IViewState): void { this.viewState = state; this.stylesheet.textContent = ` - .monaco-editor .dirty-diff-modified,.monaco-editor .dirty-diff-added{border-left-width:${state.width}px;} + .monaco-editor .dirty-diff-modified { background-size: ${state.width}px 4.5px; } + .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added{border-left-width:${state.width}px;} .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added, .monaco-editor .dirty-diff-deleted { opacity: ${state.visibility === 'always' ? 1 : 0}; } @@ -1474,8 +1475,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = if (editorGutterModifiedBackgroundColor) { collector.addRule(` .monaco-editor .dirty-diff-modified { - background-size: 3px 4.5px; - background-repeat-x: no-repeat; + background-repeat: repeat-y; background-image: linear-gradient(${linearGradient}); transition: opacity 0.5s; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 330dab052b1..757c5526c7a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -12,7 +12,7 @@ import { SCMMenus } from 'vs/workbench/contrib/scm/browser/menus'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { debounce } from 'vs/base/common/decorators'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { compareFileNames } from 'vs/base/common/comparers'; +import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { basename } from 'vs/base/common/resources'; import { binarySearch } from 'vs/base/common/arrays'; @@ -131,7 +131,12 @@ export class SCMViewService implements ISCMViewService { const name1 = getRepositoryName(workspaceContextService, op1); const name2 = getRepositoryName(workspaceContextService, op2); - return compareFileNames(name1, name2); + const nameComparison = compareFileNames(name1, name2); + if (nameComparison === 0 && op1.provider.rootUri && op2.provider.rootUri) { + return comparePaths(op1.provider.rootUri.fsPath, op2.provider.rootUri.fsPath); + } + + return nameComparison; }; scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); @@ -275,9 +280,7 @@ export class SCMViewService implements ISCMViewService { private insertRepository(repositories: ISCMRepository[], repository: ISCMRepository): void { const index = binarySearch(repositories, repository, this._compareRepositories); - if (index < 0) { - repositories.splice(~index, 0, repository); - } + repositories.splice(index < 0 ? ~index : index, 0, repository); } private onWillSaveState(): void { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index b073fc13e0d..aa6e96d9afe 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -83,7 +83,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { snippet: for (const snippet of snippets) { - if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && !snippet.prefixLow.includes(triggerCharacterLow)) { + if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && !snippet.prefixLow.startsWith(triggerCharacterLow)) { // strict -> when having trigger characters they must prefix-match continue snippet; } diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 8f9616dbd3b..540e3fce155 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -11,7 +11,6 @@ import { LanguageService } from 'vs/editor/common/services/languageService'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { CompletionContext, CompletionItemLabel, CompletionItemRanges, CompletionTriggerKind } from 'vs/editor/common/languages'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; @@ -403,7 +402,8 @@ suite('SnippetsService', function () { }); test('issue #61296: VS code freezes when editing CSS file with emoji', async function () { - disposableStore.add(LanguageConfigurationRegistry.register('fooLang'!, { + const languageConfigurationService = new TestLanguageConfigurationService(); + disposableStore.add(languageConfigurationService.register('fooLang', { wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g })); @@ -417,7 +417,7 @@ suite('SnippetsService', function () { SnippetSource.User )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); let model = disposables.add(createTextModel('.🐷-a-b', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!; @@ -548,7 +548,8 @@ suite('SnippetsService', function () { }); test('Snippet will replace auto-closing pair if specified in prefix', async function () { - disposableStore.add(LanguageConfigurationRegistry.register('fooLang'!, { + const languageConfigurationService = new TestLanguageConfigurationService(); + disposableStore.add(languageConfigurationService.register('fooLang', { brackets: [ ['{', '}'], ['[', ']'], @@ -566,7 +567,7 @@ suite('SnippetsService', function () { SnippetSource.User )]); - const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); + const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); let model = createTextModel('[psc]', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 5), context)!; @@ -728,7 +729,7 @@ suite('SnippetsService', function () { model.dispose(); }); - test('Snippets disappear with . key #145960', async function () { + test.skip('Snippets disappear with . key #145960', async function () { snippetService = new SimpleSnippetService([ new Snippet(['fooLang'], 'div', 'div', '', 'div', '', SnippetSource.User), new Snippet(['fooLang'], 'div.', 'div.', '', 'div.', '', SnippetSource.User), diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts index 22598d4aba7..043a8472b5e 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts @@ -148,7 +148,8 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { if (result) { const { uri, isDirectory } = result; const linkToOpen = { - text: matchLink, + // Use the absolute URI's path here so the optional line/col get detected + text: result.uri.fsPath + (matchLink.match(/:\d+(:\d+)?$/)?.[0] || ''), uri, bufferRange: link.bufferRange, type: link.type @@ -194,7 +195,7 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { } interface IResourceMatch { - uri: URI | undefined; + uri: URI; isDirectory?: boolean; } @@ -209,7 +210,9 @@ export class TerminalUrlLinkOpener implements ITerminalLinkOpener { if (!link.uri) { throw new Error('Tried to open a url without a resolved URI'); } - this._openerService.open(link.uri || URI.parse(link.text), { + // It's important to use the raw string value here to avoid converting pre-encoded values + // from the URL like `%2B` -> `+`. + this._openerService.open(link.text, { allowTunneling: this._isRemote, allowContributedOpeners: true, }); diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalShellIntegrationLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalShellIntegrationLinkDetector.ts index 15993914e93..c43e00e3f62 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalShellIntegrationLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalShellIntegrationLinkDetector.ts @@ -51,6 +51,9 @@ export class TerminalShellIntegrationLinkDetector implements ITerminalLinkDetect } private _matches(lines: IBufferLine[]): boolean { + if (lines.length < linkCodes.length) { + return false; + } let cell: IBufferCell | undefined; for (let i = 0; i < linkCodes.length; i++) { cell = lines[Math.floor(i / this.xterm.cols)].getCell(i % this.xterm.cols, cell); diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 2c6dcf55773..5b7471bb284 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -27,7 +27,6 @@ if [[ "$PROMPT_COMMAND" =~ .*(' '.*\;)|(\;.*' ').* ]]; then fi if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then - echo -e "\033[1;33mShell integration was disabled by the shell\033[0m" return fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index af4b8524493..02867a99753 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -81,12 +81,26 @@ z-index: 31; } -.monaco-workbench .simple-find-part-wrapper { +.xterm .xterm-screen { + cursor: text; +} + +.monaco-workbench .simple-find-part-wrapper.result-count { z-index: 33 !important; + padding-right: 80px; } -.xterm .xterm-screen { - cursor: text; +.result-count .simple-find-part { + width: 280px; + max-width: 280px; + min-width: 280px; +} + +.result-count .matchesCount { + width: 68px; + max-width: 68px; + min-width: 68px; + padding-left: 5px; } .xterm .xterm-accessibility { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 27ec4f11389..f8aae95de73 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -9,12 +9,14 @@ import 'vs/css!./media/terminal'; import 'vs/css!./media/widgets'; import 'vs/css!./media/xterm'; import * as nls from 'vs/nls'; +import { URI } from 'vs/base/common/uri'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight, KeybindingsRegistry, IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; +import { Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry, IDraggedResourceEditorInput } from 'vs/workbench/browser/dnd'; import { registerTerminalActions, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { TERMINAL_VIEW_ID, TerminalCommandId, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -22,7 +24,7 @@ import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalCol import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService, TerminalDataTransfers, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; @@ -49,6 +51,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { RemoteTerminalBackendContribution } from 'vs/workbench/contrib/terminal/browser/remoteTerminalBackend'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { TerminalMainContribution } from 'vs/workbench/contrib/terminal/browser/terminalMainContribution'; +import { Schemas } from 'vs/base/common/network'; // Register services registerSingleton(ITerminalService, TerminalService, true); @@ -81,6 +84,7 @@ workbenchRegistry.registerWorkbenchContribution(RemoteTerminalBackendContributio registerTerminalPlatformConfiguration(); registerTerminalConfiguration(); +// Register editor/dnd contributions Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(TerminalEditorInput.ID, TerminalInputSerializer); Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( @@ -92,6 +96,27 @@ Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane new SyncDescriptor(TerminalEditorInput) ] ); +Registry.as<IDragAndDropContributionRegistry>(DragAndDropExtensions.DragAndDropContribution).register({ + dataFormatKey: TerminalDataTransfers.Terminals, + getEditorInputs(data) { + const editors: IDraggedResourceEditorInput[] = []; + try { + const terminalEditors: string[] = JSON.parse(data); + for (const terminalEditor of terminalEditors) { + editors.push({ resource: URI.parse(terminalEditor) }); + } + } catch (error) { + // Invalid transfer + } + return editors; + }, + setData(resources, event) { + const terminalResources = resources.filter(({ resource }) => resource.scheme === Schemas.vscodeTerminal); + if (terminalResources.length) { + event.dataTransfer?.setData(TerminalDataTransfers.Terminals, JSON.stringify(terminalResources.map(({ resource }) => resource.toString()))); + } + } +}); // Register views const VIEW_CONTAINER = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 37ac145ece8..c48979fa4d6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -15,7 +15,7 @@ import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IEditableData } from 'vs/workbench/common/views'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IKeyMods } from 'vs/platform/quickinput/common/quickInput'; -import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -146,6 +146,7 @@ export interface ITerminalService extends ITerminalInstanceHost { onDidInputInstanceData: Event<ITerminalInstance>; onDidRegisterProcessSupport: Event<void>; onDidChangeConnectionState: Event<void>; + onDidRequestHideFindWidget: Event<void>; /** * Creates a terminal. @@ -554,6 +555,8 @@ export interface ITerminalInstance { */ onExit: Event<number | ITerminalLaunchError | undefined>; + onDidChangeFindResults: Event<{ resultIndex: number; resultCount: number } | undefined>; + readonly exitCode: number | undefined; readonly areLinksReady: boolean; @@ -649,7 +652,7 @@ export interface ITerminalInstance { /** * Copies the terminal selection to the clipboard. */ - copySelection(asHtml?: boolean): Promise<void>; + copySelection(asHtml?: boolean, command?: ITerminalCommand): Promise<void>; /** * Current selection in the terminal. @@ -864,6 +867,8 @@ export interface IXtermTerminal { */ target?: TerminalLocation; + findResult?: { resultIndex: number; resultCount: number }; + /** * Find the next instance of the term */ @@ -918,3 +923,7 @@ export const enum LinuxDistro { Fedora = 2, Ubuntu = 3, } + +export const enum TerminalDataTransfers { + Terminals = 'Terminals' +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 9fa3c648490..71eeff2a7ac 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -75,6 +75,7 @@ export class TerminalEditor extends EditorPane { this._findWidget = instantiationService.createInstance(TerminalFindWidget, this._findState); this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, _contextKeyService)); this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalEditorInstanceContext, _contextKeyService)); + this._register(this._terminalService.onDidRequestHideFindWidget(() => this.hideFindWidget())); } override async setInput(newInput: TerminalEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken) { @@ -91,6 +92,7 @@ export class TerminalEditor extends EditorPane { // panel and the editors, this is needed so that the active instance gets set // when focus changes between them. this._register(this._editorInput.terminalInstance.onDidFocus(() => this._setActiveInstance())); + this._register(this._editorInput.terminalInstance.onDidChangeFindResults(() => this._findWidget.updateResultCount())); this._editorInput.setCopyLaunchConfig(this._editorInput.terminalInstance.shellLaunchConfig); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index e4cda779a2b..6168afff93b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -7,8 +7,9 @@ import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/s import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; -import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; export class TerminalFindWidget extends SimpleFindWidget { protected _findInputFocused: IContextKey<boolean>; @@ -19,9 +20,11 @@ export class TerminalFindWidget extends SimpleFindWidget { findState: FindReplaceState, @IContextViewService _contextViewService: IContextViewService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ITerminalService private readonly _terminalService: ITerminalService + @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService ) { - super(_contextViewService, _contextKeyService, findState, true); + super(_contextViewService, _contextKeyService, findState, { showOptionButtons: true, showResultCount: true }); + this._register(findState.onFindReplaceStateChange(() => { this.show(); })); @@ -68,7 +71,24 @@ export class TerminalFindWidget extends SimpleFindWidget { if (instance) { instance.focus(); } - instance?.xterm?.clearSearchDecorations(); + // Terminals in a group currently share a find widget, so hide + // all decorations for terminals in this group + const activeGroup = this._terminalGroupService.activeGroup; + if (instance?.target !== TerminalLocation.Editor && activeGroup) { + for (const terminal of activeGroup.terminalInstances) { + terminal.xterm?.clearSearchDecorations(); + } + } else { + instance?.xterm?.clearSearchDecorations(); + } + } + + protected async _getResultCount(): Promise<{ resultIndex: number; resultCount: number } | undefined> { + const instance = this._terminalService.activeInstance; + if (instance) { + return instance.xterm?.findResult; + } + return undefined; } protected _onInputChanged() { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 4ffe4542f7c..18be1c0ae4a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -52,7 +52,7 @@ import { CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick'; -import { IRequestAddInstanceToGroupEvent, ITerminalExternalLinkProvider, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRequestAddInstanceToGroupEvent, ITerminalExternalLinkProvider, ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; @@ -329,6 +329,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { readonly onRequestAddInstanceToGroup = this._onRequestAddInstanceToGroup.event; private readonly _onDidChangeHasChildProcesses = this._register(new Emitter<boolean>()); readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; + private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>(); + readonly onDidChangeFindResults = this._onDidChangeFindResults.event; constructor( private readonly _terminalFocusContextKey: IContextKey<boolean>, @@ -444,10 +446,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._xtermReadyPromise.then(async () => { // Wait for a period to allow a container to be ready await this._containerReadyBarrier.wait(); - if (this._configHelper.config.shellIntegration?.enabled && !this.shellLaunchConfig.executable) { + + // Resolve the executable ahead of time if shell integration is enabled, this should not + // be done for custom PTYs as that would cause extension Pseudoterminal-based terminals + // to hang in resolver extensions + if (!this.shellLaunchConfig.customPtyImplementation && this._configHelper.config.shellIntegration?.enabled && !this.shellLaunchConfig.executable) { const os = await this._processManager.getBackendOS(); this.shellLaunchConfig.executable = (await this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority: this.remoteAuthority, os })).path; } + await this._createProcess(); // Re-establish the title after reconnect @@ -647,7 +654,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const lineDataEventAddon = new LineDataEventAddon(); this.xterm.raw.loadAddon(lineDataEventAddon); this.updateAccessibilitySupport(); - this.xterm.onDidRequestRunCommand(command => this.sendText(command, true)); + this.xterm.onDidRequestRunCommand(e => { + if (e.copyAsHtml) { + this.copySelection(true, e.command); + } else { + this.sendText(e.command.command, true); + } + }); // Write initial text, deferring onLineFeed listener when applicable to avoid firing // onLineData events containing initialText if (this._shellLaunchConfig.initialText) { @@ -975,6 +988,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const screenElement = xterm.attachToElement(xtermElement); + xterm.onDidChangeFindResults((results) => this._onDidChangeFindResults.fire(results)); + if (!xterm.raw.element || !xterm.raw.textarea) { throw new Error('xterm elements not set after open'); } @@ -1136,13 +1151,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return this.xterm ? this.xterm.raw.hasSelection() : false; } - async copySelection(asHtml?: boolean): Promise<void> { + async copySelection(asHtml?: boolean, command?: ITerminalCommand): Promise<void> { const xterm = await this._xtermReadyPromise; - if (this.hasSelection()) { + if (this.hasSelection() || (asHtml && command)) { if (asHtml) { - const selectionAsHtml = await xterm.getSelectionAsHtml(); + const textAsHtml = await xterm.getSelectionAsHtml(command); function listener(e: any) { - e.clipboardData.setData('text/html', selectionAsHtml); + e.clipboardData.setData('text/html', textAsHtml); e.preventDefault(); } document.addEventListener('copy', listener); @@ -1737,7 +1752,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @debounce(2000) private async _updateProcessCwd(): Promise<void> { - if (this._isDisposed) { + if (this._isDisposed || this.shellLaunchConfig.customPtyImplementation) { return; } // reset cwd if it has changed, so file based url paths can be resolved @@ -2282,7 +2297,7 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID } onDragEnter(e: DragEvent) { - if (!containsDragType(e, DataTransfers.FILES, DataTransfers.RESOURCES, DataTransfers.TERMINALS, CodeDataTransfers.FILES)) { + if (!containsDragType(e, DataTransfers.FILES, DataTransfers.RESOURCES, TerminalDataTransfers.Terminals, CodeDataTransfers.FILES)) { return; } @@ -2292,7 +2307,7 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID } // Dragging terminals - if (containsDragType(e, DataTransfers.TERMINALS)) { + if (containsDragType(e, TerminalDataTransfers.Terminals)) { const side = this._getDropSide(e); this._dropOverlay.classList.toggle('drop-before', side === 'before'); this._dropOverlay.classList.toggle('drop-after', side === 'after'); @@ -2316,7 +2331,7 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID } // Dragging terminals - if (containsDragType(e, DataTransfers.TERMINALS)) { + if (containsDragType(e, TerminalDataTransfers.Terminals)) { const side = this._getDropSide(e); this._dropOverlay.classList.toggle('drop-before', side === 'before'); this._dropOverlay.classList.toggle('drop-after', side === 'after'); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts index 873f4e0ea91..0fe61abdc4d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts @@ -95,6 +95,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider<IPick if (highlights) { return { label, + description: terminal.description, highlights: { label: highlights }, buttons: [ { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 31995a09e77..8402e0e5362 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -141,6 +141,8 @@ export class TerminalService implements ITerminalService { get onDidRegisterProcessSupport(): Event<void> { return this._onDidRegisterProcessSupport.event; } private readonly _onDidChangeConnectionState = new Emitter<void>(); get onDidChangeConnectionState(): Event<void> { return this._onDidChangeConnectionState.event; } + private readonly _onDidRequestHideFindWidget = new Emitter<void>(); + get onDidRequestHideFindWidget(): Event<void> { return this._onDidRequestHideFindWidget.event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @@ -720,6 +722,7 @@ export class TerminalService implements ITerminalService { } sourceGroup.removeInstance(source); this._terminalEditorService.openEditor(source); + this._onDidRequestHideFindWidget.fire(); } async moveToTerminalView(source?: ITerminalInstance, target?: ITerminalInstance, side?: 'before' | 'after'): Promise<void> { @@ -766,6 +769,7 @@ export class TerminalService implements ITerminalService { this._onDidChangeInstances.fire(); this._onDidChangeActiveGroup.fire(this._terminalGroupService.activeGroup); this._terminalGroupService.showPanel(true); + this._onDidRequestHideFindWidget.fire(); } protected _initInstanceListeners(instance: ITerminalInstance): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index c20833c2e64..eec5a502976 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -133,6 +133,7 @@ export class TerminalTabbedView extends Disposable { this._register(this._terminalGroupService.onDidChangeInstances(() => this._refreshShowTabs())); this._register(this._terminalGroupService.onDidChangeGroups(() => this._refreshShowTabs())); this._register(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); + this._register(this._terminalService.onDidRequestHideFindWidget(() => this.hideFindWidget())); this._updateTheme(); this._findWidget.focusTracker.onDidFocus(() => this._terminalContainer.classList.add(CssClass.FindFocus)); @@ -150,7 +151,11 @@ export class TerminalTabbedView extends Disposable { }); this._splitView = new SplitView(parentElement, { orientation: Orientation.HORIZONTAL, proportionalLayout: false }); - + this._terminalService.onDidCreateInstance(instance => { + instance.onDidChangeFindResults(() => { + this._findWidget.updateResultCount(); + }); + }); this._setupSplitView(terminalOuterContainer); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index b7a57b05460..5cf17115844 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -9,7 +9,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ITerminalGroupService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -585,13 +585,13 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop<ITerminalInstance> { // Attach terminals type to event const terminals: ITerminalInstance[] = dndData.filter(e => 'instanceId' in (e as any)); if (terminals.length > 0) { - originalEvent.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify(terminals.map(e => e.resource.toString()))); + originalEvent.dataTransfer.setData(TerminalDataTransfers.Terminals, JSON.stringify(terminals.map(e => e.resource.toString()))); } } onDragOver(data: IDragAndDropData, targetInstance: ITerminalInstance | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction { if (data instanceof NativeDragAndDropData) { - if (!containsDragType(originalEvent, DataTransfers.FILES, DataTransfers.RESOURCES, DataTransfers.TERMINALS, CodeDataTransfers.FILES)) { + if (!containsDragType(originalEvent, DataTransfers.FILES, DataTransfers.RESOURCES, TerminalDataTransfers.Terminals, CodeDataTransfers.FILES)) { return false; } } @@ -602,7 +602,7 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop<ITerminalInstance> { this._autoFocusInstance = targetInstance; } - if (!targetInstance && !containsDragType(originalEvent, DataTransfers.TERMINALS)) { + if (!targetInstance && !containsDragType(originalEvent, TerminalDataTransfers.Terminals)) { return data instanceof ElementsDragAndDropData; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalUri.ts b/src/vs/workbench/contrib/terminal/browser/terminalUri.ts index eb44e80d2f1..9a04a07b3c2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalUri.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalUri.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; -import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; export function parseTerminalUri(resource: URI): ITerminalIdentifier { const [, workspaceId, instanceId] = resource.path.split('/'); @@ -34,7 +33,7 @@ export interface IPartialDragEvent { } export function getTerminalResourcesFromDragEvent(event: IPartialDragEvent): URI[] | undefined { - const resources = event.dataTransfer?.getData(DataTransfers.TERMINALS); + const resources = event.dataTransfer?.getData(TerminalDataTransfers.Terminals); if (resources) { const json = JSON.parse(resources); const result = []; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 03f433bd72b..03cb3ead663 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -14,7 +14,7 @@ import { IThemeService, IColorTheme, registerThemingParticipant, ICssStyleCollec import { switchTerminalActionViewItemSeparator, switchTerminalShowTabsTitle } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR, TERMINAL_DRAG_AND_DROP_BACKGROUND, TERMINAL_TAB_ACTIVE_BORDER } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { ICreateTerminalOptions, ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -42,7 +42,6 @@ import { ColorScheme } from 'vs/platform/theme/common/theme'; import { getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { DataTransfers } from 'vs/base/browser/dnd'; import { getTerminalActionBarArgs } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { getShellIntegrationTooltip } from 'vs/workbench/contrib/terminal/browser/terminalTooltip'; @@ -449,7 +448,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { this._elementDisposables.push(dom.addDisposableListener(this.element, dom.EventType.DRAG_START, e => { const instance = this._terminalGroupService.activeInstance; if (e.dataTransfer && instance) { - e.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify([instance.resource.toString()])); + e.dataTransfer.setData(TerminalDataTransfers.Terminals, JSON.stringify([instance.resource.toString()])); } })); } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 678d1c5164a..40b75ff1352 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -49,7 +49,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { private _decorations: Map<number, IDisposableDecoration> = new Map(); private _placeholderDecoration: IDecoration | undefined; - private readonly _onDidRequestRunCommand = this._register(new Emitter<string>()); + private readonly _onDidRequestRunCommand = this._register(new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean }>()); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; constructor( @@ -313,10 +313,14 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { class: 'copy-output', tooltip: 'Copy Output', dispose: () => { }, id: 'terminal.copyOutput', label: localize("terminal.copyOutput", 'Copy Output'), enabled: true, run: () => this._clipboardService.writeText(command.getOutput()!) }); + actions.push({ + class: 'copy-output', tooltip: 'Copy Output as HTML', dispose: () => { }, id: 'terminal.copyOutputAsHtml', label: localize("terminal.copyOutputAsHtml", 'Copy Output as HTML'), enabled: true, + run: () => this._onDidRequestRunCommand.fire({ command, copyAsHtml: true }) + }); } actions.push({ class: 'rerun-command', tooltip: 'Rerun Command', dispose: () => { }, id: 'terminal.rerunCommand', label: localize("terminal.rerunCommand", 'Rerun Command'), enabled: true, - run: () => this._onDidRequestRunCommand.fire(command.command) + run: () => this._onDidRequestRunCommand.fire({ command }) }); return actions; } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 97171bea09d..30b72f90a8e 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -32,7 +32,7 @@ import { Color } from 'vs/base/common/color'; import { ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DecorationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/decorationAddon'; -import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { Emitter } from 'vs/base/common/event'; // How long in milliseconds should an average frame take to render for a notification to appear @@ -68,9 +68,15 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { private _webglAddon?: WebglAddonType; private _serializeAddon?: SerializeAddonType; - private readonly _onDidRequestRunCommand = new Emitter<string>(); + private _lastFindResult: { resultIndex: number; resultCount: number } | undefined; + get findResult(): { resultIndex: number; resultCount: number } | undefined { return this._lastFindResult; } + + private readonly _onDidRequestRunCommand = new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean }>(); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; + private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>(); + readonly onDidChangeFindResults = this._onDidChangeFindResults.event; + get commandTracker(): ICommandTracker { return this._commandNavigationAddon; } get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; } @@ -169,17 +175,29 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { } private _createDecorationAddon(): void { this._decorationAddon = this._instantiationService.createInstance(DecorationAddon, this._capabilities); - this._decorationAddon.onDidRequestRunCommand(command => this._onDidRequestRunCommand.fire(command)); + this._decorationAddon.onDidRequestRunCommand(e => this._onDidRequestRunCommand.fire(e)); this.raw.loadAddon(this._decorationAddon); } - async getSelectionAsHtml(): Promise<string> { + async getSelectionAsHtml(command?: ITerminalCommand): Promise<string> { if (!this._serializeAddon) { const Addon = await this._getSerializeAddonConstructor(); this._serializeAddon = new Addon(); this.raw.loadAddon(this._serializeAddon); } - return this._serializeAddon.serializeAsHTML({ onlySelection: true }); + if (command) { + const length = command.getOutput()?.length; + const row = command.marker?.line; + if (!length || !row) { + throw new Error(`No row ${row} or output length ${length} for command ${command}`); + } + await this.raw.select(0, row + 1, length - Math.floor(length / this.raw.cols)); + } + const result = this._serializeAddon.serializeAsHTML({ onlySelection: true }); + if (command) { + this.raw.clearSelection(); + } + return result; } attachToElement(container: HTMLElement): HTMLElement { @@ -292,6 +310,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { const AddonCtor = await this._getSearchAddonConstructor(); this._searchAddon = new AddonCtor(); this.raw.loadAddon(this._searchAddon); + this._searchAddon.onDidChangeResults((results: { resultIndex: number; resultCount: number } | undefined) => { + this._lastFindResult = results; + this._onDidChangeFindResults.fire(results); + }); return this._searchAddon; } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 2422169e9b5..1d3a1aa85db 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -163,7 +163,7 @@ class TerminalBackendRegistry implements ITerminalBackendRegistry { private readonly _backends = new Map<string, ITerminalBackend>(); registerTerminalBackend(backend: ITerminalBackend): void { - const key = backend.remoteAuthority ?? ''; + const key = this._sanitizeRemoteAuthority(backend.remoteAuthority); if (this._backends.has(key)) { throw new Error(`A terminal backend with remote authority '${key}' was already registered.`); } @@ -171,7 +171,12 @@ class TerminalBackendRegistry implements ITerminalBackendRegistry { } getTerminalBackend(remoteAuthority: string | undefined): ITerminalBackend | undefined { - return this._backends.get(remoteAuthority ?? ''); + return this._backends.get(this._sanitizeRemoteAuthority(remoteAuthority)); + } + + private _sanitizeRemoteAuthority(remoteAuthority: string | undefined) { + // Normalize the key to lowercase as the authority is case-insensitive + return remoteAuthority?.toLowerCase() ?? ''; } } Registry.add(TerminalExtensions.Backend, new TerminalBackendRegistry()); diff --git a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts index 55ffbaefd99..59ba5731ce5 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts @@ -27,7 +27,7 @@ export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selec light: '#00000040', dark: '#FFFFFF40', hcDark: '#FFFFFF80', - hcLight: '#F2F2F2' + hcLight: '#0F4A85' }, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.')); export const TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.defaultBackground', { light: '#00000040', diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index 04324bfe4be..aabd2add651 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -14,7 +14,7 @@ import { ByLocationTestItemElement } from 'vs/workbench/contrib/testing/browser/ import { IActionableTestTreeElement, ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; import { IComputedStateAndDurationAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; -import { InternalTestItem, TestDiffOpType, TestItemExpandState, TestResultState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, TestDiffOpType, TestItemExpandState, TestResultState, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; @@ -105,7 +105,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes // children and should trust whatever the result service gives us. const explicitComputed = item.children.size ? undefined : result.computedState; - item.retired = result.retired; item.ownState = result.ownComputedState; item.ownDuration = result.ownDuration; @@ -271,7 +270,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes const prevState = this.results.getStateById(treeElement.test.item.extId)?.[1]; if (prevState) { - treeElement.retired = prevState.retired; treeElement.ownState = prevState.computedState; treeElement.ownDuration = prevState.ownDuration; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts index e7026fbfac6..cd34647d6d0 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts @@ -9,7 +9,7 @@ import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/expl import { HierarchicalByLocationProjection as HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { ByLocationTestItemElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes'; import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper'; -import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 3634813c32b..48e0b19c426 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; -import { applyTestItemUpdate, InternalTestItem, ITestItemUpdate } from 'vs/workbench/contrib/testing/common/testCollection'; +import { applyTestItemUpdate, InternalTestItem, ITestItemUpdate } from 'vs/workbench/contrib/testing/common/testTypes'; /** * Test tree element element that groups be hierarchy. diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index 93868e0df2c..b4fa7d0aad1 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -10,7 +10,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { InternalTestItem, ITestItemContext, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestItemContext, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; /** * Describes a rendering of tests in the explorer view. Different @@ -120,11 +120,6 @@ export class TestItemTreeElement implements IActionableTestTreeElement { } /** - * Whether the node's test result is 'retired' -- from an outdated test run. - */ - public retired = false; - - /** * @inheritdoc */ public state = TestResultState.Unset; @@ -163,11 +158,11 @@ export class TestItemTreeElement implements IActionableTestTreeElement { const context: ITestItemContext = { $mid: MarshalledId.TestItemContext, - tests: [this.test], + tests: [InternalTestItem.serialize(this.test)], }; for (let p = this.parent; p && p.depth > 0; p = p.parent) { - context.tests.unshift(p.test); + context.tests.unshift(InternalTestItem.serialize(p.test)); } return context; diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay.ts index 014af9cc1d2..0a1af60895c 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { capabilityContextKeys } from 'vs/workbench/contrib/testing/common/testProfileService'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; diff --git a/src/vs/workbench/contrib/testing/browser/icons.ts b/src/vs/workbench/contrib/testing/browser/icons.ts index c469bd8c448..d4a2e945add 100644 --- a/src/vs/workbench/contrib/testing/browser/icons.ts +++ b/src/vs/workbench/contrib/testing/browser/icons.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { registerIcon, spinningLoading } from 'vs/platform/theme/common/iconRegistry'; import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { testingColorRunAction, testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme'; -import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.')); export const testingRunIcon = registerIcon('testing-run-icon', Codicon.run, localize('testingRunIcon', 'Icon of the "run test" action.')); @@ -18,7 +18,6 @@ export const testingDebugAllIcon = registerIcon('testing-debug-all-icon', Codico export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAltSmall, localize('testingDebugIcon', 'Icon of the "debug test" action.')); export const testingCancelIcon = registerIcon('testing-cancel-icon', Codicon.debugStop, localize('testingCancelIcon', 'Icon to cancel ongoing test runs.')); export const testingFilterIcon = registerIcon('testing-filter', Codicon.filter, localize('filterIcon', 'Icon for the \'Filter\' action in the testing view.')); -export const testingAutorunIcon = registerIcon('testing-autorun', Codicon.debugRerun, localize('autoRunIcon', 'Icon for the \'Autorun\' toggle in the testing view.')); export const testingHiddenIcon = registerIcon('testing-hidden', Codicon.eyeClosed, localize('hiddenIcon', 'Icon shown beside hidden tests, when they\'ve been shown.')); export const testingShowAsList = registerIcon('testing-show-as-list-icon', Codicon.listTree, localize('testingShowAsList', 'Icon shown when the test explorer is disabled as a tree.')); diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 7758973aa12..7400d289061 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -65,11 +65,6 @@ margin-right: 0.25em; } -.test-explorer .computed-state.retired, -.testing-run-glyph.retired { - opacity: 0.7 !important; -} - .test-explorer .test-is-hidden { opacity: 0.8; } diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 8230419e933..9382f47bc67 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -30,8 +30,7 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import type { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; import { TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; -import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; +import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; @@ -650,47 +649,6 @@ export class GoToTest extends Action2 { } } -abstract class ToggleAutoRun extends Action2 { - - constructor(title: string, whenToggleIs: boolean) { - super({ - id: TestCommandId.ToggleAutoRun, - title, - icon: icons.testingAutorunIcon, - toggled: whenToggleIs === true ? ContextKeyExpr.true() : ContextKeyExpr.false(), - menu: { - id: MenuId.ViewTitle, - order: ActionOrder.AutoRun, - group: 'navigation', - when: ContextKeyExpr.and( - ContextKeyExpr.equals('view', Testing.ExplorerViewId), - TestingContextKeys.autoRun.isEqualTo(whenToggleIs) - ) - } - }); - } - - /** - * @override - */ - public run(accessor: ServicesAccessor) { - accessor.get(ITestingAutoRun).toggle(); - } -} - -export class AutoRunOnAction extends ToggleAutoRun { - constructor() { - super(localize('testing.turnOnAutoRun', "Turn On Auto Run"), false); - } -} - -export class AutoRunOffAction extends ToggleAutoRun { - constructor() { - super(localize('testing.turnOffAutoRun', "Turn Off Auto Run"), true); - } -} - - abstract class ExecuteTestAtCursor extends Action2 { constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) { super({ diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index d50f2b5279a..3605c75ec80 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -28,10 +28,9 @@ import { ITestingProgressUiService, TestingProgressTrigger, TestingProgressUiSer import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration'; import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { ITestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; -import { ITestingAutoRun, TestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContentProvider } from 'vs/workbench/contrib/testing/common/testingContentProvider'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingDecorationsService } from 'vs/workbench/contrib/testing/common/testingDecorations'; @@ -50,7 +49,6 @@ registerSingleton(ITestResultStorage, TestResultStorage, true); registerSingleton(ITestProfileService, TestProfileService, true); registerSingleton(ITestResultService, TestResultService, true); registerSingleton(ITestExplorerFilterState, TestExplorerFilterState, true); -registerSingleton(ITestingAutoRun, TestingAutoRun, true); registerSingleton(ITestingOutputTerminalService, TestingOutputTerminalService, true); registerSingleton(ITestingPeekOpener, TestingPeekOpener, true); registerSingleton(ITestingProgressUiService, TestingProgressUiService, true); diff --git a/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts b/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts index 809868c283b..2427460910f 100644 --- a/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts +++ b/src/vs/workbench/contrib/testing/browser/testingConfigurationUi.ts @@ -12,7 +12,7 @@ import { QuickPickInput, IQuickPickItem, IQuickInputService, IQuickPickItemButto import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { testingUpdateProfiles } from 'vs/workbench/contrib/testing/browser/icons'; import { testConfigurationGroupNames } from 'vs/workbench/contrib/testing/common/constants'; -import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; interface IConfigurationPickerOptions { diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 546db017969..5bdc3ee229a 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -40,7 +40,7 @@ import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/work import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme'; import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { labelForTestInState, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestDiffOpType, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestDiffOpType, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestDecoration as IPublicTestDecoration, ITestingDecorationsService, TestDecorations } from 'vs/workbench/contrib/testing/common/testingDecorations'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; import { isFailedState, maxPriority } from 'vs/workbench/contrib/testing/common/testingStates'; @@ -439,7 +439,6 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] let computedState = TestResultState.Unset; let hoverMessageParts: string[] = []; let testIdWithMessages: string | undefined; - let retired = false; for (let i = 0; i < tests.length; i++) { const test = tests[i]; const resultItem = states[i]; @@ -448,7 +447,6 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] hoverMessageParts.push(labelForTestInState(test.item.label, state)); } computedState = maxPriority(computedState, state); - retired = retired || !!resultItem?.retired; if (!testIdWithMessages && resultItem?.tasks.some(t => t.messages.length)) { testIdWithMessages = test.item.extId; } @@ -462,9 +460,6 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] let hoverMessage: IMarkdownString | undefined; let glyphMarginClassName = ThemeIcon.asClassName(icon) + ' testing-run-glyph'; - if (retired) { - glyphMarginClassName += ' retired'; - } return { range: firstLineRange(range), diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 31d4e1eec73..c32661787e3 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -21,7 +21,7 @@ import { attachSuggestEnabledInputBoxStyler, ContextScopedSuggestEnabledInputWit import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { denamespaceTestTag } from 'vs/workbench/contrib/testing/common/testCollection'; +import { denamespaceTestTag } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; @@ -78,7 +78,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { const insertText = `@${ctrlId}:${tagId}`; return ({ label: `@${ctrlId}:${tagId}`, - detail: tag.ctrlLabel, + detail: this.testService.collection.getNodeById(ctrlId)?.item.label, insertText: tagId.includes(' ') ? `@${ctrlId}:"${tagId.replace(/(["\\])/g, '\\$1')}"` : insertText, }); }), @@ -205,6 +205,17 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { })), new Separator(), { + checked: this.filters.fuzzy.value, + class: undefined, + enabled: true, + id: 'fuzzy', + label: localize('testing.filters.fuzzyMatch', "Fuzzy Match"), + run: () => this.filters.fuzzy.value = !this.filters.fuzzy.value, + tooltip: '', + dispose: () => null + }, + new Separator(), + { checked: this.filters.isFilteringFor(TestFilterTerm.Hidden), class: undefined, enabled: this.testService.excluded.hasAny, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 88ad930b4bd..b742efad054 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -56,9 +56,9 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import { TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { labelForTestInState, TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; +import { labelForTestInState, TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestExplorerFilterState, TestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -513,6 +513,7 @@ export class TestingExplorerViewModel extends Disposable { this._register(Event.any( filterState.text.onDidChange, + filterState.fuzzy.onDidChange, testService.excluded.onTestExclusionsChanged, )(this.tree.refilter, this.tree)); @@ -911,13 +912,14 @@ class TestsFilter implements ITreeFilter<TestExplorerTreeElement> { return FilterResult.Include; } + const fuzzy = this.state.fuzzy.value; for (let e: TestItemTreeElement | null = element; e; e = e.parent) { // start as included if the first glob is a negation let included = this.state.globList[0].include === false ? FilterResult.Include : FilterResult.Inherit; const data = e.label.toLowerCase(); for (const { include, text } of this.state.globList) { - if (fuzzyContains(data, text)) { + if (fuzzy ? fuzzyContains(data, text) : data.includes(text)) { included = include ? FilterResult.Include : FilterResult.Exclude; } } @@ -1011,13 +1013,6 @@ const getLabelForTestTreeElement = (element: TestItemTreeElement) => { comment: ['{0} is the original label in testing.treeElementLabel, {1} is a duration'], }, '{0}, in {1}', label, formatDuration(element.duration)); } - - if (element.retired) { - label = localize({ - key: 'testing.treeElementLabelOutdated', - comment: ['{0} is the original label in testing.treeElementLabel'], - }, '{0}, outdated result', label, testStateNames[element.state]); - } } return label; @@ -1210,10 +1205,6 @@ class TestItemRenderer extends ActionableItemTemplateData<TestItemTreeElement> { : node.element.state); data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : ''); - if (node.element.retired) { - data.icon.className += ' retired'; - } - label.resource = node.element.test.item.uri; options.title = getLabelForTestTreeElement(node.element); options.fileKind = FileKind.FILE; diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 67897613b3c..71b7930768c 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -65,7 +65,7 @@ import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -349,11 +349,20 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener * Gets the first failed message that can be displayed from the result. */ private getFailedCandidateMessage(test: TestResultItem) { - return mapFindTestMessage(test, (task, message, messageIndex, taskId) => - isFailedState(task.state) && message.location - ? { taskId, index: messageIndex, message } - : undefined - ); + let best: { taskId: number; index: number; message: ITestMessage } | undefined; + mapFindTestMessage(test, (task, message, messageIndex, taskId) => { + if (!isFailedState(task.state) || !message.location) { + return; + } + + if (best && message.type !== TestMessageType.Error) { + return; + } + + best = { taskId, index: messageIndex, message }; + }); + + return best; } } diff --git a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts index 77ac8c24841..69b323858b3 100644 --- a/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts +++ b/src/vs/workbench/contrib/testing/browser/testingProgressUiService.ts @@ -13,7 +13,7 @@ import { ProgressLocation, UnmanagedProgress } from 'vs/platform/progress/common import { ViewContainerLocation } from 'vs/workbench/common/views'; import { AutoOpenTesting, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { LiveTestResult, TestResultItemChangeReason, TestStateCount } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index 02c9577f04f..dcd6e2bb567 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { contrastBorder, editorErrorForeground, editorForeground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; -import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export const testingColorIconFailed = registerColor('testing.iconFailed', { dark: '#f14c4c', diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 44511be68c4..c8fc457be2d 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; export const enum Testing { // marked as "extension" so that any existing test extensions are assigned to it. diff --git a/src/vs/workbench/contrib/testing/common/getComputedState.ts b/src/vs/workbench/contrib/testing/common/getComputedState.ts index f3d87de8eff..74597ca30c3 100644 --- a/src/vs/workbench/contrib/testing/common/getComputedState.ts +++ b/src/vs/workbench/contrib/testing/common/getComputedState.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Iterable } from 'vs/base/common/iterator'; -import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates'; /** diff --git a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts index 52ddb69be63..42e68e67209 100644 --- a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts @@ -5,12 +5,11 @@ import { Emitter } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; export class MainThreadTestCollection extends AbstractIncrementalTestCollection<IncrementalTestCollectionItem> implements IMainThreadTestCollection { private busyProvidersChangeEmitter = new Emitter<number>(); - private retireTestEmitter = new Emitter<string>(); private expandPromises = new WeakMap<IncrementalTestCollectionItem, { pendingLvl: number; doneLvl: number; @@ -43,7 +42,6 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< } public readonly onBusyProvidersChange = this.busyProvidersChangeEmitter.event; - public readonly onDidRetireTest = this.retireTestEmitter.event; constructor(private readonly expandActual: (id: string, levels: number) => Promise<void>) { super(); @@ -140,13 +138,6 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< return { ...internal, children: new Set() }; } - /** - * @override - */ - protected override retireTest(testId: string) { - this.retireTestEmitter.fire(testId); - } - private *getIterator() { const queue = [this.rootIds]; while (queue.length) { diff --git a/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 7bed594032d..53030124dfc 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; -import { IFileCoverage, CoverageDetails, ICoveredCount } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IFileCoverage, CoverageDetails, ICoveredCount } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ICoverageAccessor { provideFileCoverage: (token: CancellationToken) => Promise<IFileCoverage[]>; diff --git a/src/vs/workbench/contrib/testing/common/testExclusions.ts b/src/vs/workbench/contrib/testing/common/testExclusions.ts index 1cc222a0e5f..8479e3b95d5 100644 --- a/src/vs/workbench/contrib/testing/common/testExclusions.ts +++ b/src/vs/workbench/contrib/testing/common/testExclusions.ts @@ -9,7 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testTypes'; export class TestExclusions extends Disposable { private readonly excluded = this._register( diff --git a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts index 2f5f78039ca..0be3c3dcc8d 100644 --- a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts +++ b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts @@ -5,8 +5,10 @@ import { Emitter, Event } from 'vs/base/common/event'; import { splitGlobAware } from 'vs/base/common/glob'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { namespaceTestTag } from 'vs/workbench/contrib/testing/common/testCollection'; +import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; +import { namespaceTestTag } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ITestExplorerFilterState { _serviceBrand: undefined; @@ -36,6 +38,11 @@ export interface ITestExplorerFilterState { readonly excludeTags: ReadonlySet<string>; /** + * Whether fuzzy searching is enabled. + */ + readonly fuzzy: MutableObservableValue<boolean>; + + /** * Focuses the filter input in the test explorer view. */ focusInput(): void; @@ -81,10 +88,19 @@ export class TestExplorerFilterState implements ITestExplorerFilterState { /** @inheritdoc */ public readonly text = new MutableObservableValue(''); + /** @inheritdoc */ + public readonly fuzzy = MutableObservableValue.stored(new StoredValue<boolean>({ + key: 'testHistoryFuzzy', + scope: StorageScope.GLOBAL, + target: StorageTarget.USER, + }, this.storageService), false); + public readonly reveal = new MutableObservableValue</* test ID */string | undefined>(undefined); public readonly onDidRequestInputFocus = this.focusEmitter.event; + constructor(@IStorageService private readonly storageService: IStorageService) { } + /** @inheritdoc */ public focusInput() { this.focusEmitter.fire(); diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/testItemCollection.ts index c3409f6ba9e..06ad3b1b4b7 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testItemCollection.ts @@ -4,31 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { Barrier, isThenable, RunOnceScheduler } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { assertNever } from 'vs/base/common/types'; -// eslint-disable-next-line code-import-patterns -import { diffTestItems, ExtHostTestItemEvent, ExtHostTestItemEventOp, getPrivateApiFor, TestItemImpl, TestItemRootImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; -// eslint-disable-next-line code-import-patterns -import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { applyTestItemUpdate, ITestTag, namespaceTestTag, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; +import { applyTestItemUpdate, ITestItem, ITestTag, namespaceTestTag, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; -import * as editorRange from 'vs/editor/common/core/range'; - -type TestItemRaw = Convert.TestItem.Raw; - -export interface IHierarchyProvider { - getChildren(node: TestItemRaw, token: CancellationToken): Iterable<TestItemRaw> | AsyncIterable<TestItemRaw> | undefined | null; -} /** * @private */ -export interface OwnedCollectionTestItem { +interface CollectionItem<T> { readonly fullId: TestId; readonly parent: TestId | null; - actual: TestItemImpl; + actual: T; expand: TestItemExpandState; /** * Number of levels of items below this one that are expanded. May be infinite. @@ -37,23 +25,144 @@ export interface OwnedCollectionTestItem { resolveBarrier?: Barrier; } +export const enum TestItemEventOp { + Upsert, + SetTags, + UpdateCanResolveChildren, + RemoveChild, + SetProp, + Bulk, +} + +export interface ITestItemUpsertChild { + op: TestItemEventOp.Upsert; + item: ITestItemLike; +} + +export interface ITestItemUpdateCanResolveChildren { + op: TestItemEventOp.UpdateCanResolveChildren; + state: boolean; +} + +export interface ITestItemSetTags { + op: TestItemEventOp.SetTags; + new: ITestTag[]; + old: ITestTag[]; +} + +export interface ITestItemRemoveChild { + op: TestItemEventOp.RemoveChild; + id: string; +} + +export interface ITestItemSetProp { + op: TestItemEventOp.SetProp; + update: Partial<ITestItem>; +} +export interface ITestItemBulkReplace { + op: TestItemEventOp.Bulk; + ops: (ITestItemUpsertChild | ITestItemRemoveChild)[]; +} + +export type ExtHostTestItemEvent = + | ITestItemSetTags + | ITestItemUpsertChild + | ITestItemRemoveChild + | ITestItemUpdateCanResolveChildren + | ITestItemSetProp + | ITestItemBulkReplace; + +export interface ITestItemApi<T> { + controllerId: string; + parent?: T; + listener?: (evt: ExtHostTestItemEvent) => void; +} + +export interface ITestItemCollectionOptions<T> { + /** Controller ID to use to prefix these test items. */ + controllerId: string; + + /** Gets API for the given test item, used to listen for events and set parents. */ + getApiFor(item: T): ITestItemApi<T>; + + /** Converts the full test item to the common interface. */ + toITestItem(item: T): ITestItem; + + /** Gets children for the item. */ + getChildren(item: T): ITestChildrenLike<T>; + + /** Root to use for the new test collection. */ + root: T; +} + +const strictEqualComparator = <T>(a: T, b: T) => a === b; +const diffableProps: { [K in keyof ITestItem]?: (a: ITestItem[K], b: ITestItem[K]) => boolean } = { + range: (a, b) => { + if (a === b) { return true; } + if (!a || !b) { return false; } + return a.equalsRange(b); + }, + busy: strictEqualComparator, + label: strictEqualComparator, + description: strictEqualComparator, + error: strictEqualComparator, + tags: (a, b) => { + if (a.length !== b.length) { + return false; + } + + if (a.some(t1 => !b.includes(t1))) { + return false; + } + + return true; + }, +}; + +const diffTestItems = (a: ITestItem, b: ITestItem) => { + let output: Record<string, unknown> | undefined; + for (const [key, cmp] of Object.entries(diffableProps) as [keyof ITestItem, (a: any, b: any) => boolean][]) { + if (!cmp(a[key], b[key])) { + if (output) { + output[key] = b[key]; + } else { + output = { [key]: b[key] }; + } + } + } + + return output as Partial<ITestItem> | undefined; +}; + +export interface ITestChildrenLike<T> extends Iterable<T> { + get(id: string): T | undefined; + delete(id: string): void; +} + +export interface ITestItemLike { + id: string; + tags: readonly ITestTag[]; + canResolveChildren: boolean; +} + /** - * Maintains tests created and registered for a single set of hierarchies - * for a workspace or document. - * @private + * Maintains a collection of test items for a single controller. */ -export class SingleUseTestCollection extends Disposable { +export class TestItemCollection<T extends ITestItemLike> extends Disposable { private readonly debounceSendDiff = this._register(new RunOnceScheduler(() => this.flushDiff(), 200)); private readonly diffOpEmitter = this._register(new Emitter<TestsDiff>()); - private _resolveHandler?: (item: TestItemRaw | undefined) => Promise<void> | void; + private _resolveHandler?: (item: T | undefined) => Promise<void> | void; + + public get root() { + return this.options.root; + } - public readonly root = new TestItemRootImpl(this.controllerId, this.controllerId); - public readonly tree = new Map</* full test id */string, OwnedCollectionTestItem>(); + public readonly tree = new Map</* full test id */string, CollectionItem<T>>(); private readonly tags = new Map<string, { label?: string; refCount: number }>(); protected diff: TestsDiff = []; - constructor(private readonly controllerId: string) { + constructor(private readonly options: ITestItemCollectionOptions<T>) { super(); this.root.canResolveChildren = true; this.upsertItem(this.root, undefined); @@ -62,7 +171,7 @@ export class SingleUseTestCollection extends Disposable { /** * Handler used for expanding test items. */ - public set resolveHandler(handler: undefined | ((item: TestItemRaw | undefined) => void)) { + public set resolveHandler(handler: undefined | ((item: T | undefined) => void)) { this._resolveHandler = handler; for (const test of this.tree.values()) { this.updateExpandability(test); @@ -139,7 +248,7 @@ export class SingleUseTestCollection extends Disposable { public override dispose() { for (const item of this.tree.values()) { - getPrivateApiFor(item.actual).listener = undefined; + this.options.getApiFor(item.actual).listener = undefined; } this.tree.clear(); @@ -147,68 +256,49 @@ export class SingleUseTestCollection extends Disposable { super.dispose(); } - private onTestItemEvent(internal: OwnedCollectionTestItem, evt: ExtHostTestItemEvent) { + private onTestItemEvent(internal: CollectionItem<T>, evt: ExtHostTestItemEvent) { switch (evt.op) { - case ExtHostTestItemEventOp.Invalidated: - this.pushDiff({ op: TestDiffOpType.Retire, itemId: internal.fullId.toString() }); - break; - - case ExtHostTestItemEventOp.RemoveChild: + case TestItemEventOp.RemoveChild: this.removeItem(TestId.joinToString(internal.fullId, evt.id)); break; - case ExtHostTestItemEventOp.Upsert: - this.upsertItem(evt.item, internal); + case TestItemEventOp.Upsert: + this.upsertItem(evt.item as T, internal); break; - case ExtHostTestItemEventOp.Bulk: + case TestItemEventOp.Bulk: for (const op of evt.ops) { this.onTestItemEvent(internal, op); } break; - case ExtHostTestItemEventOp.SetProp: { - const { key, value, previous } = evt; - const extId = internal.fullId.toString(); - switch (key) { - case 'canResolveChildren': - this.updateExpandability(internal); - break; - case 'tags': - this.diffTagRefs(value, previous, extId); - break; - case 'range': - this.pushDiff({ - op: TestDiffOpType.Update, - item: { extId, item: { range: editorRange.Range.lift(Convert.Range.from(value)) } }, - }); - break; - case 'error': - this.pushDiff({ op: TestDiffOpType.Update, item: { extId, item: { error: Convert.MarkdownString.fromStrict(value) || null }, } }); - break; - default: - this.pushDiff({ op: TestDiffOpType.Update, item: { extId, item: { [key]: value ?? null } } }); - break; - } + case TestItemEventOp.SetTags: + this.diffTagRefs(evt.new, evt.old, internal.fullId.toString()); + break; + + case TestItemEventOp.UpdateCanResolveChildren: + this.updateExpandability(internal); + break; + + case TestItemEventOp.SetProp: + this.pushDiff({ + op: TestDiffOpType.Update, + item: { extId: internal.fullId.toString(), item: evt.update } + }); break; - } default: assertNever(evt); } } - private upsertItem(actual: TestItemRaw, parent: OwnedCollectionTestItem | undefined) { - if (!(actual instanceof TestItemImpl)) { - throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`); - } - + private upsertItem(actual: T, parent: CollectionItem<T> | undefined) { const fullId = TestId.fromExtHostTestItem(actual, this.root.id, parent?.actual); // If this test item exists elsewhere in the tree already (exists at an // old ID with an existing parent), remove that old item. - const privateApi = getPrivateApiFor(actual); + const privateApi = this.options.getApiFor(actual); if (privateApi.parent && privateApi.parent !== parent?.actual) { - privateApi.parent.children.delete(actual.id); + this.options.getChildren(privateApi.parent).delete(actual.id); } let internal = this.tree.get(fullId.toString()); @@ -229,9 +319,9 @@ export class SingleUseTestCollection extends Disposable { op: TestDiffOpType.Add, item: { parent: internal.parent && internal.parent.toString(), - controllerId: this.controllerId, + controllerId: this.options.controllerId, expand: internal.expand, - item: Convert.TestItem.from(actual), + item: this.options.toITestItem(actual), }, }); @@ -246,28 +336,34 @@ export class SingleUseTestCollection extends Disposable { } // Case 3: upsert of an existing item by ID, with a new instance - const oldChildren = internal.actual.children; + const oldChildren = this.options.getChildren(internal.actual); const oldActual = internal.actual; - const changedProps = diffTestItems(oldActual, actual); - getPrivateApiFor(oldActual).listener = undefined; + const update = diffTestItems(this.options.toITestItem(oldActual), this.options.toITestItem(actual)); + this.options.getApiFor(oldActual).listener = undefined; internal.actual = actual; internal.expand = TestItemExpandState.NotExpandable; // updated by `connectItemAndChildren` - for (const [key, value] of changedProps) { - this.onTestItemEvent(internal, { op: ExtHostTestItemEventOp.SetProp, key, value, previous: oldActual[key] }); + + if (update) { + // tags are handled in a special way + if (update.hasOwnProperty('tags')) { + this.diffTagRefs(actual.tags, oldActual.tags, fullId.toString()); + delete update.tags; + } + this.onTestItemEvent(internal, { op: TestItemEventOp.SetProp, update }); } this.connectItemAndChildren(actual, internal, parent); // Remove any orphaned children. for (const child of oldChildren) { - if (!actual.children.get(child.id)) { + if (!this.options.getChildren(actual).get(child.id)) { this.removeItem(TestId.joinToString(fullId, child.id)); } } } - private diffTagRefs(newTags: ITestTag[], oldTags: ITestTag[], extId: string) { + private diffTagRefs(newTags: readonly ITestTag[], oldTags: readonly ITestTag[], extId: string) { const toDelete = new Set(oldTags.map(t => t.id)); for (const tag of newTags) { if (!toDelete.delete(tag.id)) { @@ -277,7 +373,7 @@ export class SingleUseTestCollection extends Disposable { this.pushDiff({ op: TestDiffOpType.Update, - item: { extId, item: { tags: newTags.map(v => Convert.TestTag.namespace(this.controllerId, v.id)) } } + item: { extId, item: { tags: newTags.map(v => namespaceTestTag(this.options.controllerId, v.id)) } } }); toDelete.forEach(this.decrementTagRefs, this); @@ -291,8 +387,7 @@ export class SingleUseTestCollection extends Disposable { this.tags.set(tag.id, { refCount: 1 }); this.pushDiff({ op: TestDiffOpType.AddTag, tag: { - id: Convert.TestTag.namespace(this.controllerId, tag.id), - ctrlLabel: this.root.label, + id: namespaceTestTag(this.options.controllerId, tag.id), } }); } @@ -302,27 +397,27 @@ export class SingleUseTestCollection extends Disposable { const existing = this.tags.get(tagId); if (existing && !--existing.refCount) { this.tags.delete(tagId); - this.pushDiff({ op: TestDiffOpType.RemoveTag, id: namespaceTestTag(this.controllerId, tagId) }); + this.pushDiff({ op: TestDiffOpType.RemoveTag, id: namespaceTestTag(this.options.controllerId, tagId) }); } } - private setItemParent(actual: TestItemImpl, parent: OwnedCollectionTestItem | undefined) { - getPrivateApiFor(actual).parent = parent && parent.actual !== this.root ? parent.actual : undefined; + private setItemParent(actual: T, parent: CollectionItem<T> | undefined) { + this.options.getApiFor(actual).parent = parent && parent.actual !== this.root ? parent.actual : undefined; } - private connectItem(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | undefined) { + private connectItem(actual: T, internal: CollectionItem<T>, parent: CollectionItem<T> | undefined) { this.setItemParent(actual, parent); - const api = getPrivateApiFor(actual); + const api = this.options.getApiFor(actual); api.parent = parent?.actual; api.listener = evt => this.onTestItemEvent(internal, evt); this.updateExpandability(internal); } - private connectItemAndChildren(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | undefined) { + private connectItemAndChildren(actual: T, internal: CollectionItem<T>, parent: CollectionItem<T> | undefined) { this.connectItem(actual, internal, parent); // Discover any existing children that might have already been added - for (const child of actual.children) { + for (const child of this.options.getChildren(actual)) { this.upsertItem(child, internal); } } @@ -332,7 +427,7 @@ export class SingleUseTestCollection extends Disposable { * resolved state of the item changes. Can automatically expand the item * if requested by a consumer. */ - private updateExpandability(internal: OwnedCollectionTestItem) { + private updateExpandability(internal: CollectionItem<T>) { let newState: TestItemExpandState; if (!this._resolveHandler) { newState = TestItemExpandState.NotExpandable; @@ -363,13 +458,13 @@ export class SingleUseTestCollection extends Disposable { * the children will be expanded. If it's 1, the children and their children * will be expanded. If it's <0, it's a no-op. */ - private expandChildren(internal: OwnedCollectionTestItem, levels: number): Promise<void> | void { + private expandChildren(internal: CollectionItem<T>, levels: number): Promise<void> | void { if (levels < 0) { return; } const expandRequests: Promise<void>[] = []; - for (const child of internal.actual.children) { + for (const child of this.options.getChildren(internal.actual)) { const promise = this.expand(TestId.joinToString(internal.fullId, child.id), levels); if (isThenable(promise)) { expandRequests.push(promise); @@ -384,7 +479,7 @@ export class SingleUseTestCollection extends Disposable { /** * Calls `discoverChildren` on the item, refreshing all its tests. */ - private resolveChildren(internal: OwnedCollectionTestItem) { + private resolveChildren(internal: CollectionItem<T>) { if (internal.resolveBarrier) { return internal.resolveBarrier; } @@ -400,10 +495,7 @@ export class SingleUseTestCollection extends Disposable { const barrier = internal.resolveBarrier = new Barrier(); const applyError = (err: Error) => { - console.error(`Unhandled error in resolveHandler of test controller "${this.controllerId}"`); - if (internal.actual !== this.root) { - internal.actual.error = err.stack || err.message || String(err); - } + console.error(`Unhandled error in resolveHandler of test controller "${this.options.controllerId}"`, err); }; let r: Thenable<void> | void; @@ -426,7 +518,7 @@ export class SingleUseTestCollection extends Disposable { return internal.resolveBarrier; } - private pushExpandStateUpdate(internal: OwnedCollectionTestItem) { + private pushExpandStateUpdate(internal: CollectionItem<T>) { this.pushDiff({ op: TestDiffOpType.Update, item: { extId: internal.fullId.toString(), expand: internal.expand } }); } @@ -438,21 +530,21 @@ export class SingleUseTestCollection extends Disposable { this.pushDiff({ op: TestDiffOpType.Remove, itemId: childId }); - const queue: (OwnedCollectionTestItem | undefined)[] = [childItem]; + const queue: (CollectionItem<T> | undefined)[] = [childItem]; while (queue.length) { const item = queue.pop(); if (!item) { continue; } - getPrivateApiFor(item.actual).listener = undefined; + this.options.getApiFor(item.actual).listener = undefined; for (const tag of item.actual.tags) { this.decrementTagRefs(tag.id); } this.tree.delete(item.fullId.toString()); - for (const child of item.actual.children) { + for (const child of this.options.getChildren(item.actual)) { queue.push(this.tree.get(TestId.joinToString(item.fullId, child.id))); } } @@ -468,3 +560,120 @@ export class SingleUseTestCollection extends Disposable { } } } + +/** Implementation os vscode.TestItemCollection */ +export interface ITestItemChildren<T extends ITestItemLike> extends Iterable<T> { + readonly size: number; + replace(items: readonly T[]): void; + forEach(callback: (item: T, collection: this) => unknown, thisArg?: unknown): void; + add(item: T): void; + delete(itemId: string): void; + get(itemId: string): T | undefined; + + toJSON(): readonly T[]; +} + +export class DuplicateTestItemError extends Error { + constructor(id: string) { + super(`Attempted to insert a duplicate test item ID ${id}`); + } +} + +export class InvalidTestItemError extends Error { + constructor(id: string) { + super(`TestItem with ID "${id}" is invalid. Make sure to create it from the createTestItem method.`); + } +} + +export class MixedTestItemController extends Error { + constructor(id: string, ctrlA: string, ctrlB: string) { + super(`TestItem with ID "${id}" is from controller "${ctrlA}" and cannot be added as a child of an item from controller "${ctrlB}".`); + } +} + +export const createTestItemChildren = <T extends ITestItemLike>(api: ITestItemApi<T>, getApi: (item: T) => ITestItemApi<T>, checkCtor: Function): ITestItemChildren<T> => { + let mapped = new Map<string, T>(); + + return { + /** @inheritdoc */ + get size() { + return mapped.size; + }, + + /** @inheritdoc */ + forEach(callback: (item: T, collection: ITestItemChildren<T>) => unknown, thisArg?: unknown) { + for (const item of mapped.values()) { + callback.call(thisArg, item, this); + } + }, + + /** @inheritdoc */ + replace(items: Iterable<T>) { + const newMapped = new Map<string, T>(); + const toDelete = new Set(mapped.keys()); + const bulk: ITestItemBulkReplace = { op: TestItemEventOp.Bulk, ops: [] }; + + for (const item of items) { + if (!(item instanceof checkCtor)) { + throw new InvalidTestItemError(item.id); + } + + const itemController = getApi(item).controllerId; + if (itemController !== api.controllerId) { + throw new MixedTestItemController(item.id, itemController, api.controllerId); + } + + if (newMapped.has(item.id)) { + throw new DuplicateTestItemError(item.id); + } + + newMapped.set(item.id, item); + toDelete.delete(item.id); + bulk.ops.push({ op: TestItemEventOp.Upsert, item }); + } + + for (const id of toDelete.keys()) { + bulk.ops.push({ op: TestItemEventOp.RemoveChild, id }); + } + + api.listener?.(bulk); + + // important mutations come after firing, so if an error happens no + // changes will be "saved": + mapped = newMapped; + }, + + + /** @inheritdoc */ + add(item: T) { + if (!(item instanceof checkCtor)) { + throw new InvalidTestItemError(item.id); + } + + mapped.set(item.id, item); + api.listener?.({ op: TestItemEventOp.Upsert, item }); + }, + + /** @inheritdoc */ + delete(id: string) { + if (mapped.delete(id)) { + api.listener?.({ op: TestItemEventOp.RemoveChild, id }); + } + }, + + /** @inheritdoc */ + get(itemId: string) { + return mapped.get(itemId); + }, + + /** JSON serialization function. */ + toJSON() { + return Array.from(mapped.values()); + }, + + /** @inheritdoc */ + [Symbol.iterator]() { + return mapped.values(); + }, + }; +}; diff --git a/src/vs/workbench/contrib/testing/common/testProfileService.ts b/src/vs/workbench/contrib/testing/common/testProfileService.ts index 7c83d34d371..ecdb9ffcd63 100644 --- a/src/vs/workbench/contrib/testing/common/testProfileService.ts +++ b/src/vs/workbench/contrib/testing/common/testProfileService.ts @@ -9,7 +9,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { InternalTestItem, ITestRunProfile, TestRunProfileBitset, testRunProfileBitsetList } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, ITestRunProfile, TestRunProfileBitset, testRunProfileBitsetList } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { IMainThreadTestController } from 'vs/workbench/contrib/testing/common/testService'; diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index ad80e64dff4..dce023d7d60 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { localize } from 'vs/nls'; import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; import { IObservableValue, MutableObservableValue, staticObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { IRichLocation, ISerializedTestResults, ITestItem, ITestMessage, ITestOutputMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestItemExpandState, TestMessageType, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { IRichLocation, ISerializedTestResults, ITestItem, ITestMessage, ITestOutputMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestItemExpandState, TestMessageType, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { maxPriority, statesInOrder, terminalStatePriorities } from 'vs/workbench/contrib/testing/common/testingStates'; @@ -230,18 +230,15 @@ const itemToNode = (controllerId: string, item: ITestItem, parent: string | null tasks: [], ownComputedState: TestResultState.Unset, computedState: TestResultState.Unset, - retired: false, }); export const enum TestResultItemChangeReason { - Retired, - ParentRetired, ComputedStateChange, OwnStateChange, } export type TestResultItemChange = { item: TestResultItem; result: ITestResult } & ( - | { reason: TestResultItemChangeReason.Retired | TestResultItemChangeReason.ParentRetired | TestResultItemChangeReason.ComputedStateChange } + | { reason: TestResultItemChangeReason.ComputedStateChange } | { reason: TestResultItemChangeReason.OwnStateChange; previousState: TestResultState; previousOwnDuration: number | undefined } ); @@ -420,33 +417,6 @@ export class LiveTestResult implements ITestResult { } /** - * Marks a test as retired. This can trigger it to be rerun in live mode. - */ - public retire(testId: string) { - const root = this.testById.get(testId); - if (!root || root.retired) { - return; - } - - const queue = [[root]]; - while (queue.length) { - for (const entry of queue.pop()!) { - if (!entry.retired) { - entry.retired = true; - queue.push(entry.children); - this.changeEmitter.fire({ - result: this, - item: entry, - reason: entry === root - ? TestResultItemChangeReason.Retired - : TestResultItemChangeReason.ParentRetired - }); - } - } - } - } - - /** * Marks the task in the test run complete. */ public markTaskComplete(taskId: string) { @@ -638,7 +608,7 @@ export class HydratedTestResult implements ITestResult { this.request = serialized.request; for (const item of serialized.items) { - const cast: TestResultItem = { ...item, retired: true } as any; + const cast: TestResultItem = { ...item } as any; cast.item.uri = URI.revive(cast.item.uri); for (const task of cast.tasks) { diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 061f3694be8..5c3211c0ad1 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -11,7 +11,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ExtensionRunTestsRequest, ITestRunProfile, ResolvedTestRunRequest, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; diff --git a/src/vs/workbench/contrib/testing/common/testResultStorage.ts b/src/vs/workbench/contrib/testing/common/testResultStorage.ts index 6c272186949..d2d294aa081 100644 --- a/src/vs/workbench/contrib/testing/common/testResultStorage.ts +++ b/src/vs/workbench/contrib/testing/common/testResultStorage.ts @@ -14,7 +14,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { ISerializedTestResults } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ISerializedTestResults } from 'vs/workbench/contrib/testing/common/testTypes'; import { HydratedTestResult, ITestResult, LiveOutputController, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; export const RETAIN_MAX_RESULTS = 128; diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 68509583b60..7c2450a2e94 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; -import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, RunTestForControllerRequest, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, ITestItemContext, ResolvedTestRunRequest, RunTestForControllerRequest, TestItemExpandState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 083d40f7801..efc2afbd0ca 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -17,7 +17,7 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestExclusions } from 'vs/workbench/contrib/testing/common/testExclusions'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 4a50a4c7d9e..6f3e639ef90 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -229,7 +229,7 @@ export interface ITestRunTask { } export interface ITestTag { - id: string; + readonly id: string; } const testTagDelimiter = '\0'; @@ -244,7 +244,6 @@ export const denamespaceTestTag = (namespaced: string) => { export interface ITestTagDisplayInfo { id: string; - ctrlLabel: string; } /** @@ -419,8 +418,6 @@ export interface TestResultItem extends InternalTestItem { ownComputedState: TestResultState; /** Computed state based on children */ computedState: TestResultState; - /** True if the test is outdated */ - retired: boolean; /** Max duration of the item's tasks (if run directly) */ ownDuration?: number; } @@ -564,7 +561,7 @@ export interface ITestItemContext { /** Marshalling marker */ $mid: MarshalledId.TestItemContext; /** Tests and parents from the root to the current items */ - tests: InternalTestItem[]; + tests: InternalTestItem.Serialized[]; } /** diff --git a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts deleted file mode 100644 index 905b7370f17..00000000000 --- a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts +++ /dev/null @@ -1,148 +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 { RunOnceScheduler } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { AutoRunMode, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { InternalTestItem, TestDiffOpType, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; -import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; -import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; -import { isRunningTests, ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { getCollectionItemParents, ITestService } from 'vs/workbench/contrib/testing/common/testService'; - -export interface ITestingAutoRun { - /** - * Toggles autorun on or off. - */ - toggle(): void; -} - -export const ITestingAutoRun = createDecorator<ITestingAutoRun>('testingAutoRun'); - -export class TestingAutoRun extends Disposable implements ITestingAutoRun { - private enabled: IContextKey<boolean>; - private runner = this._register(new MutableDisposable()); - - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @ITestService private readonly testService: ITestService, - @ITestResultService private readonly results: ITestResultService, - @IConfigurationService private readonly configuration: IConfigurationService, - ) { - super(); - this.enabled = TestingContextKeys.autoRun.bindTo(contextKeyService); - - this._register(configuration.onDidChangeConfiguration(evt => { - if (evt.affectsConfiguration(TestingConfigKeys.AutoRunMode) && this.enabled.get()) { - this.runner.value = this.makeRunner(); - } - })); - } - - /** - * @inheritdoc - */ - public toggle(): void { - const enabled = this.enabled.get(); - if (enabled) { - this.runner.value = undefined; - } else { - this.runner.value = this.makeRunner(); - } - - this.enabled.set(!enabled); - } - - /** - * Creates the runner. Is triggered when tests are marked as retired. - * Runs them on a debounce. - */ - private makeRunner() { - const rerunIds = new Map<string, InternalTestItem>(); - const store = new DisposableStore(); - const cts = new CancellationTokenSource(); - store.add(toDisposable(() => cts.dispose(true))); - - let delay = getTestingConfiguration(this.configuration, TestingConfigKeys.AutoRunDelay); - - store.add(this.configuration.onDidChangeConfiguration(() => { - delay = getTestingConfiguration(this.configuration, TestingConfigKeys.AutoRunDelay); - })); - - const scheduler = store.add(new RunOnceScheduler(async () => { - if (rerunIds.size === 0) { - return; - } - - const tests = [...rerunIds.values()]; - rerunIds.clear(); - await this.testService.runTests({ group: TestRunProfileBitset.Run, tests, isAutoRun: true }); - - if (rerunIds.size > 0) { - scheduler.schedule(delay); - } - }, delay)); - - const addToRerun = (test: InternalTestItem) => { - rerunIds.set(test.item.extId, test); - if (!isRunningTests(this.results)) { - scheduler.schedule(delay); - } - }; - - const removeFromRerun = (test: InternalTestItem) => { - rerunIds.delete(test.item.extId); - if (rerunIds.size === 0) { - scheduler.cancel(); - } - }; - - store.add(this.results.onTestChanged(evt => { - if (evt.reason === TestResultItemChangeReason.Retired) { - addToRerun(evt.item); - } else if ((evt.reason === TestResultItemChangeReason.OwnStateChange || evt.reason === TestResultItemChangeReason.ComputedStateChange)) { - removeFromRerun(evt.item); - } - })); - - store.add(this.results.onResultsChanged(evt => { - if ('completed' in evt && !isRunningTests(this.results) && rerunIds.size) { - scheduler.schedule(0); - } - })); - - if (getTestingConfiguration(this.configuration, TestingConfigKeys.AutoRunMode) === AutoRunMode.AllInWorkspace) { - - store.add(this.testService.onDidProcessDiff(diff => { - for (const entry of diff) { - if (entry.op === TestDiffOpType.Add) { - const test = entry.item; - const isQueued = Iterable.some( - getCollectionItemParents(this.testService.collection, test), - t => rerunIds.has(test.item.extId), - ); - - const state = this.results.getStateById(test.item.extId); - if (!isQueued && (!state || state[1].retired)) { - addToRerun(test); - } - } - } - })); - - - for (const root of this.testService.collection.rootItems) { - addToRerun(root); - } - } - - return store; - } -} diff --git a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index 2f64387a111..c93c0968a07 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -9,7 +9,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { TestMessageType } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestMessageType } from 'vs/workbench/contrib/testing/common/testTypes'; import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index d43cb0e7af1..2663b018541 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TestExplorerViewMode, TestExplorerViewSorting } from 'vs/workbench/contrib/testing/common/constants'; -import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; export namespace TestingContextKeys { export const providerCount = new RawContextKey('testing.providerCount', 0); diff --git a/src/vs/workbench/contrib/testing/common/testingDecorations.ts b/src/vs/workbench/contrib/testing/common/testingDecorations.ts index 98abb6b86ec..3a148eb3e97 100644 --- a/src/vs/workbench/contrib/testing/common/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/common/testingDecorations.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestMessage } from 'vs/workbench/contrib/testing/common/testTypes'; export interface ITestingDecorationsService { _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts index b3fcba06951..4d999302c9e 100644 --- a/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts +++ b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; export interface ITestingPeekOpener { diff --git a/src/vs/workbench/contrib/testing/common/testingStates.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts index cb322d13399..38ea70ce4bf 100644 --- a/src/vs/workbench/contrib/testing/common/testingStates.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export type TreeStateNode = { statusNode: true; state: TestResultState; priority: number }; @@ -16,8 +16,8 @@ export const statePriority: { [K in TestResultState]: number } = { [TestResultState.Running]: 6, [TestResultState.Errored]: 5, [TestResultState.Failed]: 4, - [TestResultState.Passed]: 3, - [TestResultState.Queued]: 2, + [TestResultState.Queued]: 3, + [TestResultState.Passed]: 2, [TestResultState.Unset]: 1, [TestResultState.Skipped]: 0, }; diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts index 66bf4f2db54..af584d316c4 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts @@ -7,11 +7,11 @@ import * as assert from 'assert'; import { AbstractTreeViewState } from 'vs/base/browser/ui/tree/abstractTree'; import { Emitter } from 'vs/base/common/event'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; -import { TestDiffOpType, TestItemExpandState, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestDiffOpType, TestItemExpandState, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; -import { Convert, TestItemImpl } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; +import { TestTestItem } from 'vs/workbench/contrib/testing/test/common/testStubs'; class TestHierarchicalByLocationProjection extends HierarchicalByLocationProjection { } @@ -55,10 +55,10 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { harness.flush(); harness.pushDiff({ op: TestDiffOpType.Add, - item: { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('ctrl2', 'c', 'c', undefined)) }, + item: { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: new TestTestItem('ctrl2', 'c', 'c').toTestItem() }, }, { op: TestDiffOpType.Add, - item: { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('ctrl2', 'c-a', 'ca', undefined)) }, + item: { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: new TestTestItem('ctrl2', 'c-a', 'ca').toTestItem() }, }); assert.deepStrictEqual(harness.flush(), [ @@ -76,7 +76,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { { e: 'b' } ]); - harness.c.root.children.get('id-a')!.children.add(new TestItemImpl('ctrlId', 'ac', 'ac', undefined)); + harness.c.root.children.get('id-a')!.children.add(new TestTestItem('ctrlId', 'ac', 'ac')); assert.deepStrictEqual(harness.flush(), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] }, @@ -119,7 +119,6 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { }, parent: 'id-root', tasks: [], - retired: false, ownComputedState: state, computedState: state, expand: 0, diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts index 8e5b439a920..ec4424b8edd 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts @@ -7,11 +7,11 @@ import * as assert from 'assert'; import { AbstractTreeViewState } from 'vs/base/browser/ui/tree/abstractTree'; import { Emitter } from 'vs/base/common/event'; import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; -import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestResultItemChange } from 'vs/workbench/contrib/testing/common/testResult'; -import { Convert, TestItemImpl } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree'; +import { TestTestItem } from 'vs/workbench/contrib/testing/test/common/testStubs'; suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { let harness: TestTreeTestHarness<HierarchicalByNameProjection>; @@ -44,10 +44,10 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { harness.flush(); harness.pushDiff({ op: TestDiffOpType.Add, - item: { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('ctrl2', 'c', 'root2', undefined)) }, + item: { controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: new TestTestItem('ctrl2', 'c', 'root2').toTestItem() }, }, { op: TestDiffOpType.Add, - item: { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('ctrl2', 'c-a', 'c', undefined)) }, + item: { controllerId: 'ctrl2', parent: new TestId(['ctrl2', 'c']).toString(), expand: TestItemExpandState.NotExpandable, item: new TestTestItem('ctrl2', 'c-a', 'c', undefined).toTestItem() }, }); assert.deepStrictEqual(harness.flush(), [ @@ -59,7 +59,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { test('updates nodes if they add children', async () => { harness.flush(); - harness.c.root.children.get('id-a')!.children.add(new TestItemImpl('ctrl2', 'ac', 'ac', undefined)); + harness.c.root.children.get('id-a')!.children.add(new TestTestItem('ctrl2', 'ac', 'ac')); assert.deepStrictEqual(harness.flush(), [ { e: 'aa' }, @@ -81,7 +81,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { test('swaps when node is no longer leaf', async () => { harness.flush(); - harness.c.root.children.get('id-b')!.children.add(new TestItemImpl('ctrl2', 'ba', 'ba', undefined)); + harness.c.root.children.get('id-b')!.children.add(new TestTestItem('ctrl2', 'ba', 'ba')); assert.deepStrictEqual(harness.flush(), [ { e: 'aa' }, diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index d5dd271b36e..9fd9b315d93 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -9,7 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; -import { TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; diff --git a/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts deleted file mode 100644 index ce29ffa8119..00000000000 --- a/src/vs/workbench/contrib/testing/test/common/ownedTestCollection.ts +++ /dev/null @@ -1,17 +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 { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; - -export class TestSingleUseCollection extends SingleUseTestCollection { - public get currentDiff() { - return this.diff; - } - - public setDiff(diff: TestsDiff) { - this.diff = diff; - } -} diff --git a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts index 1b4657d5600..cc89323ebdd 100644 --- a/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testExplorerFilterState.test.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; import { TestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; suite('TestExplorerFilterState', () => { let t: TestExplorerFilterState; setup(() => { - t = new TestExplorerFilterState(); + t = new TestExplorerFilterState(new InMemoryStorageService()); }); const assertFilteringFor = (expected: { [T in TestFilterTerm]?: boolean }) => { diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index ea9d89f01ce..4452c491436 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -9,14 +9,13 @@ import { bufferToStream, newWriteableBufferStream, VSBuffer } from 'vs/base/comm import { Lazy } from 'vs/base/common/lazy'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { NullLogService } from 'vs/platform/log/common/log'; -import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { ITestTaskState, ResolvedTestRunRequest, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestTaskState, ResolvedTestRunRequest, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { Convert, getInitializedMainTestCollection, TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; +import { getInitializedMainTestCollection, testStubs, TestTestCollection } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; export const emptyOutputController = () => new LiveOutputController( @@ -32,7 +31,7 @@ suite('Workbench - Test Results Service', () => { let r: TestLiveTestResult; let changed = new Set<TestResultItemChange>(); - let tests: SingleUseTestCollection; + let tests: TestTestCollection; const defaultOpts = (testIds: string[]): ResolvedTestRunRequest => ({ targets: [{ @@ -72,16 +71,15 @@ suite('Workbench - Test Results Service', () => { throw new Error('timed out while expanding, diff: ' + JSON.stringify(tests.collectDiff())); } - r.addTestChainToRun('ctrlId', [ - Convert.TestItem.from(tests.root), - Convert.TestItem.from(tests.root.children.get('id-a') as TestItemImpl), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa') as TestItemImpl), + tests.root.toTestItem(), + tests.root.children.get('id-a')!.toTestItem(), + tests.root.children.get('id-a')!.children.get('id-aa')!.toTestItem(), ]); r.addTestChainToRun('ctrlId', [ - Convert.TestItem.from(tests.root.children.get('id-a') as TestItemImpl), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-ab') as TestItemImpl), + tests.root.children.get('id-a')!.toTestItem(), + tests.root.children.get('id-a')!.children.get('id-ab')!.toTestItem(), ]); }); @@ -171,20 +169,6 @@ suite('Workbench - Test Results Service', () => { assert.deepStrictEqual(r.getStateById(testId)?.ownComputedState, TestResultState.Errored); }); - test('retire', () => { - changed.clear(); - r.retire(new TestId(['ctrlId', 'id-a']).toString()); - assert.deepStrictEqual(getChangeSummary(), [ - { label: 'a', reason: TestResultItemChangeReason.Retired }, - { label: 'aa', reason: TestResultItemChangeReason.ParentRetired }, - { label: 'ab', reason: TestResultItemChangeReason.ParentRetired }, - ]); - - changed.clear(); - r.retire(new TestId(['ctrlId', 'id-a']).toString()); - assert.strictEqual(changed.size, 0); - }); - test('ignores outside run', () => { changed.clear(); r.updateState(new TestId(['ctrlId', 'id-b']).toString(), 't', TestResultState.Running); @@ -252,7 +236,7 @@ suite('Workbench - Test Results Service', () => { const expected: any = { ...r.getStateById(tests.root.id)! }; expected.item.uri = actual.item.uri; expected.item.children = actual.item.children; - assert.deepStrictEqual(actual, { ...expected, retired: true, children: [new TestId(['ctrlId', 'id-a']).toString()] }); + assert.deepStrictEqual(actual, { ...expected, children: [new TestId(['ctrlId', 'id-a']).toString()] }); assert.deepStrictEqual(rehydrated.counts, r.counts); assert.strictEqual(typeof rehydrated.completedAt, 'number'); }); diff --git a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts index 0fd486588e7..e10d994eb03 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -9,8 +9,8 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { Convert, TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { emptyOutputController } from 'vs/workbench/contrib/testing/test/common/testResultService.test'; +import { testStubs } from 'vs/workbench/contrib/testing/test/common/testStubs'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Workbench - Test Result Storage', () => { @@ -28,9 +28,9 @@ suite('Workbench - Test Result Storage', () => { const tests = testStubs.nested(); tests.expand(tests.root.id, Infinity); t.addTestChainToRun('ctrlId', [ - Convert.TestItem.from(tests.root), - Convert.TestItem.from(tests.root.children.get('id-a') as TestItemImpl), - Convert.TestItem.from(tests.root.children.get('id-a')!.children.get('id-aa') as TestItemImpl), + tests.root.toTestItem(), + tests.root.children.get('id-a')!.toTestItem(), + tests.root.children.get('id-a')!.children.get('id-aa')!.toTestItem(), ]); if (addMessage) { diff --git a/src/vs/workbench/contrib/testing/test/common/testStubs.ts b/src/vs/workbench/contrib/testing/test/common/testStubs.ts index 9ef3d39af94..9c9bb140bc2 100644 --- a/src/vs/workbench/contrib/testing/test/common/testStubs.ts +++ b/src/vs/workbench/contrib/testing/test/common/testStubs.ts @@ -4,15 +4,95 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -// eslint-disable-next-line code-import-patterns -import { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection'; -import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; +import { ITestItem, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { createTestItemChildren, ITestItemApi, ITestItemLike, TestItemCollection, TestItemEventOp } from 'vs/workbench/contrib/testing/common/testItemCollection'; -// eslint-disable-next-line code-import-patterns -export * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -// eslint-disable-next-line code-import-patterns -export { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi'; +export class TestTestItem implements ITestItemLike { + private readonly props: ITestItem; + private _canResolveChildren = false; + + public get tags() { + return this.props.tags.map(id => ({ id })); + } + + public set tags(value) { + this.api.listener?.({ op: TestItemEventOp.SetTags, new: value, old: this.props.tags.map(t => ({ id: t })) }); + this.props.tags = value.map(tag => tag.id); + } + + public get canResolveChildren() { + return this._canResolveChildren; + } + + public set canResolveChildren(value: boolean) { + this._canResolveChildren = value; + this.api.listener?.({ op: TestItemEventOp.UpdateCanResolveChildren, state: value }); + } + + public get parent() { + return this.api.parent; + } + + public api: ITestItemApi<TestTestItem> = { controllerId: this.controllerId }; + + public children = createTestItemChildren(this.api, i => i.api, TestTestItem); + + constructor( + public readonly controllerId: string, + public readonly id: string, + label: string, + uri?: URI, + ) { + this.props = { + extId: '', + busy: false, + description: null, + error: null, + label, + range: null, + sortText: null, + tags: [], + uri, + }; + } + + public get<K extends keyof ITestItem>(key: K): ITestItem[K] { + return this.props[key]; + } + + public set<K extends keyof ITestItem>(key: K, value: ITestItem[K]) { + this.props[key] = value; + this.api.listener?.({ op: TestItemEventOp.SetProp, update: { [key]: value } }); + } + + public toTestItem(): ITestItem { + const props = { ...this.props }; + props.extId = TestId.fromExtHostTestItem(this, this.controllerId).toString(); + return props; + } +} + +export class TestTestCollection extends TestItemCollection<TestTestItem> { + constructor(controllerId = 'ctrlId') { + super({ + controllerId, + getApiFor: t => t.api, + toITestItem: t => t.toTestItem(), + getChildren: t => t.children, + root: new TestTestItem(controllerId, controllerId, 'root'), + }); + } + + public get currentDiff() { + return this.diff; + } + + public setDiff(diff: TestsDiff) { + this.diff = diff; + } +} /** * Gets a main thread test collection initialized with the given set of @@ -27,19 +107,17 @@ export const getInitializedMainTestCollection = async (singleUse = testStubs.nes export const testStubs = { nested: (idPrefix = 'id-') => { - const collection = new TestSingleUseCollection('ctrlId'); - collection.root.label = 'root'; + const collection = new TestTestCollection(); collection.resolveHandler = item => { if (item === undefined) { - const a = new TestItemImpl('ctrlId', idPrefix + 'a', 'a', URI.file('/')); + const a = new TestTestItem('ctrlId', idPrefix + 'a', 'a', URI.file('/')); a.canResolveChildren = true; - const b = new TestItemImpl('ctrlId', idPrefix + 'b', 'b', URI.file('/')); - collection.root.children.replace([a, b]); + const b = new TestTestItem('ctrlId', idPrefix + 'b', 'b', URI.file('/')); + collection.root.children.add(a); + collection.root.children.add(b); } else if (item.id === idPrefix + 'a') { - item.children.replace([ - new TestItemImpl('ctrlId', idPrefix + 'aa', 'aa', URI.file('/')), - new TestItemImpl('ctrlId', idPrefix + 'ab', 'ab', URI.file('/')), - ]); + item.children.add(new TestTestItem('ctrlId', idPrefix + 'aa', 'aa', URI.file('/'))); + item.children.add(new TestTestItem('ctrlId', idPrefix + 'ab', 'ab', URI.file('/'))); } }; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index efb577a072b..1c312d73bdd 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -476,7 +476,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } if (this._webviewFindWidget) { - parent.appendChild(this._webviewFindWidget.getDomNode()!); + parent.appendChild(this._webviewFindWidget.getDomNode()); } parent.appendChild(this.element); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts index fde2df743dd..e7bad42e252 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts @@ -20,6 +20,9 @@ export interface WebviewFindDelegate { } export class WebviewFindWidget extends SimpleFindWidget { + protected async _getResultCount(dataChanged?: boolean): Promise<{ resultIndex: number; resultCount: number } | undefined> { + return undefined; + } protected readonly _findWidgetFocused: IContextKey<boolean>; @@ -28,7 +31,7 @@ export class WebviewFindWidget extends SimpleFindWidget { @IContextViewService contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService ) { - super(contextViewService, contextKeyService, undefined, false, _delegate.checkImeCompletionState); + super(contextViewService, contextKeyService, undefined, { showOptionButtons: false, checkImeCompletionState: _delegate.checkImeCompletionState }); this._findWidgetFocused = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); this._register(_delegate.hasFindResult(hasResult => { diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index f7beac7c5bb..e6a1076d917 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -6,7 +6,7 @@ import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -23,10 +23,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; -import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; +import { IViewBadge, IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { IOverlayWebview, IWebviewService, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewWindowDragMonitor } from 'vs/workbench/contrib/webview/browser/webviewWindowDragMonitor'; import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; +import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; declare const ResizeObserver: any; @@ -48,6 +49,9 @@ export class WebviewViewPane extends ViewPane { private readonly defaultTitle: string; private setTitle: string | undefined; + private badge: IViewBadge | undefined; + private activity: IDisposable | undefined; + private readonly memento: Memento; private readonly viewState: MementoObject; private readonly extensionId?: ExtensionIdentifier; @@ -69,6 +73,7 @@ export class WebviewViewPane extends ViewPane { @IWebviewService private readonly webviewService: IWebviewService, @IWebviewViewService private readonly webviewViewService: IWebviewViewService, @IViewsService private readonly viewService: IViewsService, + @IActivityService private activityService: IActivityService ) { super({ ...options, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.extensionId = options.fromExtensionId; @@ -211,6 +216,9 @@ export class WebviewViewPane extends ViewPane { get description(): string | undefined { return self.titleDescription; }, set description(value: string | undefined) { self.updateTitleDescription(value); }, + get badge(): IViewBadge | undefined { return self.badge; }, + set badge(badge: IViewBadge | undefined) { self.updateBadge(badge); }, + dispose: () => { // Only reset and clear the webview itself. Don't dispose of the view container this._activated = false; @@ -232,6 +240,28 @@ export class WebviewViewPane extends ViewPane { super.updateTitle(typeof value === 'string' ? value : this.defaultTitle); } + protected updateBadge(badge: IViewBadge | undefined) { + + if (this.badge?.value === badge?.value && + this.badge?.tooltip === badge?.tooltip) { + return; + } + + if (this.activity) { + this.activity.dispose(); + this.activity = undefined; + } + + this.badge = badge; + if (badge) { + const activity = { + badge: new NumberBadge(badge.value, () => badge.tooltip), + priority: 150 + }; + this.activityService.showViewActivity(this.id, activity); + } + } + private async withProgress(task: () => Promise<void>): Promise<void> { return this.progressService.withProgress({ location: this.id, delay: 500 }, task); } diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts index c3565f919d5..c4ebff8bfee 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts @@ -7,6 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IViewBadge } from 'vs/workbench/common/views'; import { IOverlayWebview } from 'vs/workbench/contrib/webview/browser/webview'; export const IWebviewViewService = createDecorator<IWebviewViewService>('webviewViewService'); @@ -14,6 +15,7 @@ export const IWebviewViewService = createDecorator<IWebviewViewService>('webview export interface WebviewView { title?: string; description?: string; + badge?: IViewBadge; readonly webview: IOverlayWebview; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts index e8339183da5..d73cfe79d1c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts @@ -23,7 +23,7 @@ suite('Getting Started Markdown Renderer', () => { const rendered = await renderer.renderMarkdown(mdPath, mdBase); const imageSrcs = [...rendered.matchAll(/img src="[^"]*"/g)].map(match => match[0]); for (const src of imageSrcs) { - const targetSrcFormat = /^img src="https:\/\/file%2B.vscode-resource.vscode-webview.net\/.*\/vs\/workbench\/contrib\/welcomeGettingStarted\/common\/media\/.*.png"$/; + const targetSrcFormat = /^img src="https:\/\/file%2B.vscode-resource.vscode-cdn.net\/.*\/vs\/workbench\/contrib\/welcomeGettingStarted\/common\/media\/.*.png"$/; assert(targetSrcFormat.test(src), `${src} didnt match regex`); } languageService.dispose(); |