diff options
author | Johannes Rieken <johannes.rieken@gmail.com> | 2022-07-01 16:55:43 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-01 16:55:43 +0300 |
commit | 268c941bf0fd8255d4dd7c106c22e9b911772916 (patch) | |
tree | f518885bf72335e46535da8ac3b3aee78a0be190 /src/vs/workbench/contrib | |
parent | 7599f3bdf2bbb708c95139e9da6421d5969c9a83 (diff) | |
parent | 48fef0c1da52ea1c603355cbf97e722455460e7b (diff) |
Merge branch 'main' into joh/cellUri
Diffstat (limited to 'src/vs/workbench/contrib')
433 files changed, 16672 insertions, 6654 deletions
diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts b/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts index ec03a265487..dcac8bf037b 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts @@ -52,7 +52,7 @@ export class AudioCueService extends Disposable implements IAudioCueService { } private getVolumeInPercent(): number { - let volume = this.configurationService.getValue<number>('audioCues.volume'); + const volume = this.configurationService.getValue<number>('audioCues.volume'); if (typeof volume !== 'number') { return 50; } diff --git a/src/vs/workbench/contrib/audioCues/browser/observable.ts b/src/vs/workbench/contrib/audioCues/browser/observable.ts index 937f4b2f9cf..2ad62412411 100644 --- a/src/vs/workbench/contrib/audioCues/browser/observable.ts +++ b/src/vs/workbench/contrib/audioCues/browser/observable.ts @@ -6,7 +6,9 @@ import { Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -export interface IObservable<T> { +export interface IObservable<T, TChange = void> { + _change: TChange; + /** * Reads the current value. * @@ -39,7 +41,7 @@ export interface IReader { * * Is called by `Observable.read`. */ - handleBeforeReadObservable<T>(observable: IObservable<T>): void; + handleBeforeReadObservable<T>(observable: IObservable<T, any>): void; } export interface IObserver { @@ -61,7 +63,7 @@ export interface IObserver { * Implementations must not call into other observables! * The change should be processed when {@link IObserver.endUpdate} is called. */ - handleChange<T>(observable: IObservable<T>): void; + handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void; /** * Indicates that an update operation has completed. @@ -69,8 +71,8 @@ export interface IObserver { endUpdate<T>(observable: IObservable<T>): void; } -export interface ISettable<T> { - set(value: T, transaction: ITransaction | undefined): void; +export interface ISettable<T, TChange = void> { + set(value: T, transaction: ITransaction | undefined, change: TChange): void; } export interface ITransaction { @@ -80,12 +82,14 @@ export interface ITransaction { */ updateObserver( observer: IObserver, - observable: IObservable<any> + observable: IObservable<any, any> ): void; } // === Base === -export abstract class ConvenientObservable<T> implements IObservable<T> { +export abstract class ConvenientObservable<T, TChange> implements IObservable<T, TChange> { + get _change(): TChange { return null!; } + public abstract get(): T; public abstract subscribe(observer: IObserver): void; public abstract unsubscribe(observer: IObserver): void; @@ -100,7 +104,7 @@ export abstract class ConvenientObservable<T> implements IObservable<T> { } } -export abstract class BaseObservable<T> extends ConvenientObservable<T> { +export abstract class BaseObservable<T, TChange = void> extends ConvenientObservable<T, TChange> { protected readonly observers = new Set<IObserver>(); public subscribe(observer: IObserver): void { @@ -151,9 +155,9 @@ class TransactionImpl implements ITransaction { } } -export class ObservableValue<T> - extends BaseObservable<T> - implements ISettable<T> +export class ObservableValue<T, TChange = void> + extends BaseObservable<T, TChange> + implements ISettable<T, TChange> { private value: T; @@ -166,14 +170,14 @@ export class ObservableValue<T> return this.value; } - public set(value: T, tx: ITransaction | undefined): void { + public set(value: T, tx: ITransaction | undefined, change: TChange): void { if (this.value === value) { return; } if (!tx) { transaction((tx) => { - this.set(value, tx); + this.set(value, tx, change); }); return; } @@ -182,7 +186,7 @@ export class ObservableValue<T> for (const observer of this.observers) { tx.updateObserver(observer, this); - observer.handleChange(this); + observer.handleChange(this, change); } } } @@ -191,7 +195,7 @@ export function constObservable<T>(value: T): IObservable<T> { return new ConstObservable(value); } -class ConstObservable<T> extends ConvenientObservable<T> { +class ConstObservable<T> extends ConvenientObservable<T, void> { constructor(private readonly value: T) { super(); } @@ -208,18 +212,35 @@ class ConstObservable<T> extends ConvenientObservable<T> { } // == autorun == -export function autorun( - fn: (reader: IReader) => void, - name: string +export function autorun(fn: (reader: IReader) => void, name: string): IDisposable { + return new AutorunObserver(fn, name, undefined); +} + +interface IChangeContext { + readonly changedObservable: IObservable<any, any>; + readonly change: unknown; + + didChange<T, TChange>(observable: IObservable<T, TChange>): this is { change: TChange }; +} + +export function autorunHandleChanges( + name: string, + options: { + /** + * Returns if this change should cause a re-run of the autorun. + */ + handleChange: (context: IChangeContext) => boolean; + }, + fn: (reader: IReader) => void ): IDisposable { - return new AutorunObserver(fn, name); + return new AutorunObserver(fn, name, options.handleChange); } export function autorunWithStore( fn: (reader: IReader, store: DisposableStore) => void, name: string ): IDisposable { - let store = new DisposableStore(); + const store = new DisposableStore(); const disposable = autorun( reader => { store.clear(); @@ -252,7 +273,8 @@ export class AutorunObserver implements IObserver, IReader, IDisposable { constructor( private readonly runFn: (reader: IReader) => void, - public readonly name: string + public readonly name: string, + private readonly _handleChange: ((context: IChangeContext) => boolean) | undefined ) { this.runIfNeeded(); } @@ -264,8 +286,13 @@ export class AutorunObserver implements IObserver, IReader, IDisposable { } } - public handleChange() { - this.needsToRun = true; + public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void { + const shouldReact = this._handleChange ? this._handleChange({ + changedObservable: observable, + change, + didChange: o => o === observable as any, + }) : true; + this.needsToRun = this.needsToRun || shouldReact; if (this.updateCount === 0) { this.runIfNeeded(); @@ -337,12 +364,12 @@ export function autorunDelta<T>( export function derivedObservable<T>(name: string, computeFn: (reader: IReader) => T): IObservable<T> { return new LazyDerived(computeFn, name); } -export class LazyDerived<T> extends ConvenientObservable<T> { +export class LazyDerived<T> extends ConvenientObservable<T, void> { private readonly observer: LazyDerivedObserver<T>; constructor(computeFn: (reader: IReader) => T, name: string) { super(); - this.observer = new LazyDerivedObserver(computeFn, name); + this.observer = new LazyDerivedObserver(computeFn, name, this); } public subscribe(observer: IObserver): void { @@ -366,7 +393,7 @@ export class LazyDerived<T> extends ConvenientObservable<T> { * @internal */ class LazyDerivedObserver<T> - extends BaseObservable<T> + extends BaseObservable<T, void> implements IReader, IObserver { private hadValue = false; private hasValue = false; @@ -385,7 +412,8 @@ class LazyDerivedObserver<T> constructor( private readonly computeFn: (reader: IReader) => T, - public readonly name: string + public readonly name: string, + private readonly actualObservable: LazyDerived<T>, ) { super(); } @@ -486,9 +514,8 @@ class LazyDerivedObserver<T> this.hasValue = true; if (this.hadValue && oldValue !== this.value) { - // for (const r of this.observers) { - r.handleChange(this); + r.handleChange(this.actualObservable, undefined); } } } @@ -508,6 +535,20 @@ export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ val return observable; } +export function waitForState<T, TState extends T>(observable: IObservable<T>, predicate: (state: T) => state is TState): Promise<TState>; +export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean): Promise<T>; +export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean): Promise<T> { + return new Promise(resolve => { + const d = autorun(reader => { + const currentState = observable.read(reader); + if (predicate(currentState)) { + d.dispose(); + resolve(currentState); + } + }, 'waitForState'); + }); +} + export function observableFromEvent<T, TArgs = unknown>( event: Event<TArgs>, getValue: (args: TArgs | undefined) => T @@ -540,7 +581,7 @@ class FromEventObservable<TArgs, T> extends BaseObservable<T> { transaction(tx => { for (const o of this.observers) { tx.updateObserver(o, this); - o.handleChange(this); + o.handleChange(this, undefined); } }); } @@ -612,3 +653,38 @@ export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number, return observable; } + +/** + * This ensures the observable is kept up-to-date. + * This is useful when the observables `get` method is used. +*/ +export function keepAlive(observable: IObservable<any>): IDisposable { + return autorun(reader => { + observable.read(reader); + }, 'keep-alive'); +} + +export function derivedObservableWithCache<T>(name: string, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> { + let lastValue: T | undefined = undefined; + const observable = derivedObservable(name, reader => { + lastValue = computeFn(reader, lastValue); + return lastValue; + }); + return observable; +} + +export function derivedObservableWithWritableCache<T>(name: string, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> & { clearCache(transaction: ITransaction): void } { + let lastValue: T | undefined = undefined; + const counter = new ObservableValue(0, 'counter'); + const observable = derivedObservable(name, reader => { + counter.read(reader); + lastValue = computeFn(reader, lastValue); + return lastValue; + }); + return Object.assign(observable, { + clearCache: (transaction: ITransaction) => { + lastValue = undefined; + counter.set(counter.get() + 1, transaction); + }, + }); +} diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts index 602b81639e2..1f533a82ccc 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -41,7 +41,7 @@ export class BulkCellEdits { const resources: URI[] = []; const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); - for (let group of editsByNotebook) { + for (const group of editsByNotebook) { if (this._token.isCancellationRequested) { break; } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index aaab23b4da9..e4dd996202c 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -48,10 +48,10 @@ class BulkEdit { ariaMessage(): string { - let otherResources = new ResourceMap<boolean>(); - let textEditResources = new ResourceMap<boolean>(); + const otherResources = new ResourceMap<boolean>(); + const textEditResources = new ResourceMap<boolean>(); let textEditCount = 0; - for (let edit of this._edits) { + for (const edit of this._edits) { if (edit instanceof ResourceTextEdit) { textEditCount += 1; textEditResources.set(edit.resource, true); @@ -95,7 +95,7 @@ class BulkEdit { const resources: (readonly URI[])[] = []; let index = 0; - for (let range of ranges) { + for (const range of ranges) { if (this._token.isCancellationRequested) { break; } @@ -177,7 +177,7 @@ export class BulkEditService implements IBulkEditService { let codeEditor = options?.editor; // try to find code editor if (!codeEditor) { - let candidate = this._editorService.activeTextEditorControl; + const candidate = this._editorService.activeTextEditorControl; if (isCodeEditor(candidate)) { codeEditor = candidate; } @@ -194,7 +194,7 @@ export class BulkEditService implements IBulkEditService { let undoRedoGroup: UndoRedoGroup | undefined; let undoRedoGroupRemove = () => { }; if (typeof options?.undoRedoGroupId === 'number') { - for (let candidate of this._activeUndoRedoGroups) { + for (const candidate of this._activeUndoRedoGroups) { if (candidate.id === options.undoRedoGroupId) { undoRedoGroup = candidate; break; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index 86de76556d2..2cd2d328bd3 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -366,7 +366,7 @@ export class BulkFileEdits { } } - for (let group of groups) { + for (const group of groups) { if (this._token.isCancellationRequested) { break; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index b1ff888c894..f69e60201f2 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -19,15 +19,19 @@ import { ResourceMap } from 'vs/base/common/map'; import { IModelService } from 'vs/editor/common/services/model'; import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { performSnippetEdits } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; type ValidationResult = { canApply: true } | { canApply: false; reason: URI }; +type ISingleSnippetEditOperation = ISingleEditOperation & { insertAsSnippet?: boolean }; + class ModelEditTask implements IDisposable { readonly model: ITextModel; private _expectedModelVersionId: number | undefined; - protected _edits: ISingleEditOperation[]; + protected _edits: ISingleSnippetEditOperation[]; protected _newEol: EndOfLineSequence | undefined; constructor(private readonly _modelReference: IReference<IResolvedTextEditorModel>) { @@ -75,7 +79,7 @@ class ModelEditTask implements IDisposable { } else { range = Range.lift(textEdit.range); } - this._edits.push(EditOperation.replaceMove(range, textEdit.text)); + this._edits.push({ ...EditOperation.replaceMove(range, textEdit.text), insertAsSnippet: textEdit.insertAsSnippet }); } validate(): ValidationResult { @@ -91,13 +95,29 @@ class ModelEditTask implements IDisposable { apply(): void { if (this._edits.length > 0) { - this._edits = this._edits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + this._edits = this._edits + .map(this._transformSnippetStringToInsertText, this) // no editor -> no snippet mode + .sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); this.model.pushEditOperations(null, this._edits, () => null); } if (this._newEol !== undefined) { this.model.pushEOL(this._newEol); } } + + protected _transformSnippetStringToInsertText(edit: ISingleSnippetEditOperation): ISingleSnippetEditOperation { + // transform a snippet edit (and only those) into a normal text edit + // for that we need to parse the snippet and get its actual text, e.g without placeholder + // or variable syntaxes + if (!edit.insertAsSnippet) { + return edit; + } + if (!edit.text) { + return edit; + } + const text = new SnippetParser().parse(edit.text, false, false).toString(); + return { ...edit, insertAsSnippet: false, text }; + } } class EditorEditTask extends ModelEditTask { @@ -121,10 +141,19 @@ class EditorEditTask extends ModelEditTask { super.apply(); return; } - if (this._edits.length > 0) { - this._edits = this._edits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - this._editor.executeEdits('', this._edits); + + const insertAsSnippet = this._edits.every(edit => edit.insertAsSnippet); + if (insertAsSnippet) { + // todo@jrieken what ABOUT EOL? + performSnippetEdits(this._editor, this._edits.map(edit => ({ range: Range.lift(edit.range!), snippet: edit.text! }))); + + } else { + this._edits = this._edits + .map(this._transformSnippetStringToInsertText, this) // mixed edits (snippet and normal) -> no snippet mode + .sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + this._editor.executeEdits('', this._edits); + } } if (this._newEol !== undefined) { if (this._editor.hasModel()) { @@ -170,9 +199,9 @@ export class BulkTextEdits { private _validateBeforePrepare(): void { // First check if loaded models were not changed in the meantime for (const array of this._edits.values()) { - for (let edit of array) { + for (const edit of array) { if (typeof edit.versionId === 'number') { - let model = this._modelService.getModel(edit.resource); + const model = this._modelService.getModel(edit.resource); if (model && model.getVersionId() !== edit.versionId) { // model changed in the meantime throw new Error(`${model.uri.toString()} has changed in the meantime`); @@ -187,13 +216,13 @@ export class BulkTextEdits { const tasks: ModelEditTask[] = []; const promises: Promise<any>[] = []; - for (let [key, value] of this._edits) { + for (const [key, value] of this._edits) { const promise = this._textModelResolverService.createModelReference(key).then(async ref => { let task: ModelEditTask; let makeMinimal = false; if (this._editor?.getModel()?.uri.toString() === ref.object.textEditorModel.uri.toString()) { task = new EditorEditTask(ref, this._editor); - makeMinimal = true; + makeMinimal = true && false; // todo@jrieken HACK } else { task = new ModelEditTask(ref); } @@ -204,7 +233,7 @@ export class BulkTextEdits { if (!newEdits) { task.addEdit(edit); } else { - for (let moreMinialEdit of newEdits) { + for (const moreMinialEdit of newEdits) { task.addEdit(new ResourceTextEdit(edit.resource, moreMinialEdit, edit.versionId, edit.metadata)); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts b/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts index 11364522dd2..fd18958888b 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts @@ -31,7 +31,7 @@ export class ConflictDetector { const _workspaceEditResources = new ResourceMap<boolean>(); - for (let edit of edits) { + for (const edit of edits) { if (edit instanceof ResourceTextEdit) { _workspaceEditResources.set(edit.resource, true); if (typeof edit.versionId === 'number') { @@ -81,7 +81,7 @@ export class ConflictDetector { this._onDidConflict.fire(this); } }; - for (let model of modelService.getModels()) { + for (const model of modelService.getModels()) { this._disposables.add(model.onDidChangeContent(() => onDidChangeModel(model))); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts index 9ebd49c836a..bad36b7ccad 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts @@ -62,11 +62,11 @@ class UXState { // (2) close preview editors if (editors) { - for (let group of this._editorGroupsService.groups) { - let previewEditors: EditorInput[] = []; - for (let input of group.editors) { + for (const group of this._editorGroupsService.groups) { + const previewEditors: EditorInput[] = []; + for (const input of group.editors) { - let resource = EditorResourceAccessor.getCanonicalUri(input, { supportSideBySide: SideBySideEditor.PRIMARY }); + const resource = EditorResourceAccessor.getCanonicalUri(input, { supportSideBySide: SideBySideEditor.PRIMARY }); if (resource?.scheme === BulkEditPreviewProvider.Schema) { previewEditors.push(input); } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index c6af0ffa259..6aed9f2eb8f 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -124,7 +124,7 @@ export class BulkEditPane extends ViewPane { contentContainer.appendChild(treeContainer); this._treeDataSource = this._instaService.createInstance(BulkEditDataSource); - this._treeDataSource.groupByFile = this._storageService.getBoolean(BulkEditPane._memGroupByFile, StorageScope.GLOBAL, true); + this._treeDataSource.groupByFile = this._storageService.getBoolean(BulkEditPane._memGroupByFile, StorageScope.PROFILE, true); this._ctxGroupByFile.set(this._treeDataSource.groupByFile); this._tree = <WorkbenchAsyncDataTree<BulkFileOperations, BulkEditElement, FuzzyScore>>this._instaService.createInstance( @@ -272,9 +272,7 @@ export class BulkEditPane extends ViewPane { } private _done(accept: boolean): void { - if (this._currentResolve) { - this._currentResolve(accept ? this._currentInput?.getWorkspaceEdit() : undefined); - } + this._currentResolve?.(accept ? this._currentInput?.getWorkspaceEdit() : undefined); this._currentInput = undefined; this._setState(State.Message); this._sessionDisposables.clear(); @@ -304,7 +302,7 @@ export class BulkEditPane extends ViewPane { if (input) { // (1) capture view state - let oldViewState = this._tree.getViewState(); + const oldViewState = this._tree.getViewState(); this._treeViewStates.set(this._treeDataSource.groupByFile, oldViewState); // (2) toggle and update @@ -312,7 +310,7 @@ export class BulkEditPane extends ViewPane { this._setTreeInput(input); // (3) remember preference - this._storageService.store(BulkEditPane._memGroupByFile, this._treeDataSource.groupByFile, StorageScope.GLOBAL, StorageTarget.USER); + this._storageService.store(BulkEditPane._memGroupByFile, this._treeDataSource.groupByFile, StorageScope.PROFILE, StorageTarget.USER); this._ctxGroupByFile.set(this._treeDataSource.groupByFile); } } @@ -322,7 +320,7 @@ export class BulkEditPane extends ViewPane { -readonly [P in keyof T]: T[P] }; - let options: Mutable<ITextEditorOptions> = { ...e.editorOptions }; + const options: Mutable<ITextEditorOptions> = { ...e.editorOptions }; let fileElement: FileElement; if (e.element instanceof TextEditElement) { fileElement = e.element.parent; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts index e562290513f..c3d21f6233b 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts @@ -104,7 +104,7 @@ export class BulkFileOperation { } needsConfirmation(): boolean { - for (let [, edit] of this.originalEdits) { + for (const [, edit] of this.originalEdits) { if (!this.parent.checked.isChecked(edit)) { return true; } @@ -238,7 +238,7 @@ export class BulkFileOperations { insert(uri, operationByResource); // insert into "this" category - let key = BulkCategory.keyOf(edit.metadata); + const key = BulkCategory.keyOf(edit.metadata); let category = operationByCategory.get(key); if (!category) { category = new BulkCategory(edit.metadata); @@ -253,7 +253,7 @@ export class BulkFileOperations { // "correct" invalid parent-check child states that is // unchecked file edits (rename, create, delete) uncheck // all edits for a file, e.g no text change without rename - for (let file of this.fileOperations) { + for (const file of this.fileOperations) { if (file.type !== BulkFileOperationType.TextEdit) { let checked = true; for (const edit of file.originalEdits.values()) { @@ -307,7 +307,7 @@ export class BulkFileOperations { getFileEdits(uri: URI): ISingleEditOperation[] { - for (let file of this.fileOperations) { + for (const file of this.fileOperations) { if (file.uri.toString() === uri.toString()) { const result: ISingleEditOperation[] = []; @@ -336,7 +336,7 @@ export class BulkFileOperations { } getUriOfEdit(edit: ResourceEdit): URI { - for (let file of this.fileOperations) { + for (const file of this.fileOperations) { for (const value of file.originalEdits.values()) { if (value === edit) { return file.uri; @@ -382,7 +382,7 @@ export class BulkEditPreviewProvider implements ITextModelContentProvider { } private async _init() { - for (let operation of this._operations.fileOperations) { + for (const operation of this._operations.fileOperations) { await this._applyTextEditsToPreviewModel(operation.uri); } this._disposables.add(this._operations.checked.onDidChange(e => { @@ -395,7 +395,7 @@ export class BulkEditPreviewProvider implements ITextModelContentProvider { const model = await this._getOrCreatePreviewModel(uri); // undo edits that have been done before - let undoEdits = this._modelPreviewEdits.get(model.id); + const undoEdits = this._modelPreviewEdits.get(model.id); if (undoEdits) { model.applyEdits(undoEdits); } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index 4c4641c83e0..0bacf852e4c 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -54,7 +54,7 @@ export class FileElement implements ICheckable { ) { } isChecked(): boolean { - let model = this.parent instanceof CategoryElement ? this.parent.parent : this.parent; + const model = this.parent instanceof CategoryElement ? this.parent.parent : this.parent; let checked = true; @@ -64,7 +64,7 @@ export class FileElement implements ICheckable { } // multiple file edits -> reflect single state - for (let edit of this.edit.originalEdits.values()) { + for (const edit of this.edit.originalEdits.values()) { if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } @@ -72,8 +72,8 @@ export class FileElement implements ICheckable { // multiple categories and text change -> read all elements if (this.parent instanceof CategoryElement && this.edit.type === BulkFileOperationType.TextEdit) { - for (let category of model.categories) { - for (let file of category.fileOperations) { + for (const category of model.categories) { + for (const file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { if (edit instanceof ResourceFileEdit) { @@ -89,15 +89,15 @@ export class FileElement implements ICheckable { } setChecked(value: boolean): void { - let model = this.parent instanceof CategoryElement ? this.parent.parent : this.parent; + const model = this.parent instanceof CategoryElement ? this.parent.parent : this.parent; for (const edit of this.edit.originalEdits.values()) { model.checked.updateChecked(edit, value); } // multiple categories and file change -> update all elements if (this.parent instanceof CategoryElement && this.edit.type !== BulkFileOperationType.TextEdit) { - for (let category of model.categories) { - for (let file of category.fileOperations) { + for (const category of model.categories) { + for (const file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { model.checked.updateChecked(edit, value); @@ -110,10 +110,10 @@ export class FileElement implements ICheckable { isDisabled(): boolean { if (this.parent instanceof CategoryElement && this.edit.type === BulkFileOperationType.TextEdit) { - let model = this.parent.parent; + const model = this.parent.parent; let checked = true; - for (let category of model.categories) { - for (let file of category.fileOperations) { + for (const category of model.categories) { + for (const file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { if (edit instanceof ResourceFileEdit) { @@ -227,14 +227,14 @@ export class BulkEditDataSource implements IAsyncDataSource<BulkFileOperations, const range = Range.lift(edit.textEdit.textEdit.range); //prefix-math - let startTokens = textModel.tokenization.getLineTokens(range.startLineNumber); + const startTokens = textModel.tokenization.getLineTokens(range.startLineNumber); let prefixLen = 23; // default value for the no tokens/grammar case for (let idx = startTokens.findTokenIndexAtOffset(range.startColumn - 1) - 1; prefixLen < 50 && idx >= 0; idx--) { prefixLen = range.startColumn - startTokens.getStartOffset(idx); } //suffix-math - let endTokens = textModel.tokenization.getLineTokens(range.endLineNumber); + const endTokens = textModel.tokenization.getLineTokens(range.endLineNumber); let suffixLen = 0; for (let idx = endTokens.findTokenIndexAtOffset(range.endColumn - 1); suffixLen < 50 && idx < endTokens.getCount(); idx++) { suffixLen += endTokens.getEndOffset(idx) - endTokens.getStartOffset(idx); @@ -583,11 +583,11 @@ class TextEditElementTemplate { value += element.inserting; value += element.suffix; - let selectHighlight: IHighlight = { start: element.prefix.length, end: element.prefix.length + element.selecting.length, extraClasses: ['remove'] }; - let insertHighlight: IHighlight = { start: selectHighlight.end, end: selectHighlight.end + element.inserting.length, extraClasses: ['insert'] }; + const selectHighlight: IHighlight = { start: element.prefix.length, end: element.prefix.length + element.selecting.length, extraClasses: ['remove'] }; + const insertHighlight: IHighlight = { start: selectHighlight.end, end: selectHighlight.end + element.inserting.length, extraClasses: ['insert'] }; let title: string | undefined; - let { metadata } = element.edit.textEdit; + const { metadata } = element.edit.textEdit; if (metadata && metadata.description) { title = localize('title', "{0} - {1}", metadata.label, metadata.description); } else if (metadata) { diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts index 2b22fc4d10c..7f643688baf 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts @@ -92,7 +92,7 @@ class CallHierarchyController implements IEditorContribution { const cts = new CancellationTokenSource(); const model = CallHierarchyModel.create(document, position, cts.token); - const direction = sanitizedDirection(this._storageService.get(CallHierarchyController._StorageDirection, StorageScope.GLOBAL, CallHierarchyDirection.CallsTo)); + const direction = sanitizedDirection(this._storageService.get(CallHierarchyController._StorageDirection, StorageScope.PROFILE, CallHierarchyDirection.CallsTo)); this._showCallHierarchyWidget(position, direction, model, cts); } @@ -130,7 +130,7 @@ class CallHierarchyController implements IEditorContribution { this._widget.showLoading(); this._sessionDisposables.add(this._widget.onDidClose(() => { this.endCallHierarchy(); - this._storageService.store(CallHierarchyController._StorageDirection, this._widget!.direction, StorageScope.GLOBAL, StorageTarget.USER); + this._storageService.store(CallHierarchyController._StorageDirection, this._widget!.direction, StorageScope.PROFILE, StorageTarget.USER); })); this._sessionDisposables.add({ dispose() { cts.dispose(true); } }); this._sessionDisposables.add(this._widget); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index f1e945e1bef..34a848d551b 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -44,11 +44,11 @@ const enum State { class LayoutInfo { static store(info: LayoutInfo, storageService: IStorageService): void { - storageService.store('callHierarchyPeekLayout', JSON.stringify(info), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('callHierarchyPeekLayout', JSON.stringify(info), StorageScope.PROFILE, StorageTarget.MACHINE); } static retrieve(storageService: IStorageService): LayoutInfo { - const value = storageService.get('callHierarchyPeekLayout', StorageScope.GLOBAL, '{}'); + const value = storageService.get('callHierarchyPeekLayout', StorageScope.PROFILE, '{}'); const defaultInfo: LayoutInfo = { ratio: 0.7, height: 17 }; try { return { ...defaultInfo, ...JSON.parse(value) }; @@ -163,7 +163,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { const editorContainer = document.createElement('div'); editorContainer.classList.add('editor'); container.appendChild(editorContainer); - let editorOptions: IEditorOptions = { + const editorOptions: IEditorOptions = { scrollBeyondLastLine: false, scrollbar: { verticalScrollbarSize: 14, @@ -320,7 +320,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { this._editor.setModel(value.object.textEditorModel); // set decorations for caller ranges (if in the same file) - let decorations: IModelDeltaDecoration[] = []; + const decorations: IModelDeltaDecoration[] = []; let fullRange: IRange | undefined; let locations = element.locations; if (!locations) { diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts index bce1b7846dd..5175aa33d6d 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts @@ -110,7 +110,7 @@ export class CallRenderer implements ITreeRenderer<Call, FuzzyScore, CallRenderi renderTemplate(container: HTMLElement): CallRenderingTemplate { container.classList.add('callhierarchy-element'); - let icon = document.createElement('div'); + const icon = document.createElement('div'); container.appendChild(icon); const label = new IconLabel(container, { supportHighlights: true }); return new CallRenderingTemplate(icon, label); @@ -119,6 +119,7 @@ export class CallRenderer implements ITreeRenderer<Call, FuzzyScore, CallRenderi renderElement(node: ITreeNode<Call, FuzzyScore>, _index: number, template: CallRenderingTemplate): void { const { element, filterData } = node; const deprecated = element.item.tags?.includes(SymbolTag.Deprecated); + template.icon.className = ''; template.icon.classList.add('inline', ...CSSIcon.asClassNameArray(SymbolKinds.toIcon(element.item.kind))); template.label.setLabel( element.item.name, diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index 86b60ef52a3..d2542d854ba 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -177,7 +177,7 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { } private _descriptionForCommand(commandId: string, msg: string, noKbMsg: string): string { - let kb = this._keybindingService.lookupKeybinding(commandId); + const kb = this._keybindingService.lookupKeybinding(commandId); if (kb) { return strings.format(msg, kb.getAriaLabel()); } @@ -265,7 +265,7 @@ class AccessibilityHelpWidget extends Widget implements IOverlayWidget { } private _layout(): void { - let editorLayout = this._editor.getLayoutInfo(); + const editorLayout = this._editor.getLayoutInfo(); const width = Math.min(editorLayout.width - 40, AccessibilityHelpWidget.WIDTH); const height = Math.min(editorLayout.height - 40, AccessibilityHelpWidget.HEIGHT); diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts b/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts index 6a08839d154..6803bea8f1a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/editorSettingsMigration.ts @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorSettingMigration, ISettingsWriter } from 'vs/editor/browser/config/migrateOptions'; -import { ConfigurationKeyValuePairs, Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configurationMigration'; +import { ConfigurationKeyValuePairs, Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; Registry.as<IConfigurationMigrationRegistry>(Extensions.ConfigurationMigration) .registerConfigurationMigrations(EditorSettingMigration.items.map(item => ({ diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index 46b11d7eedf..cc120227164 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -9,7 +9,6 @@ position: absolute; top: 0; right: 18px; - width: 220px; max-width: calc(100% - 28px - 28px - 8px); pointer-events: none; padding: 0 10px 10px; diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 5fa3bb659ab..bc5c6adca32 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -22,6 +22,8 @@ import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; import * as strings from 'vs/base/common/strings'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint'; +import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find (\u21C5 for history)"); @@ -80,9 +82,9 @@ export abstract class SimpleFindWidget extends Widget { }, appendCaseSensitiveLabel: options.appendCaseSensitiveLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindCaseSensitive) : undefined, appendRegexLabel: options.appendRegexLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindRegex) : undefined, - appendWholeWordsLabel: options.appendWholeWordsLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindWholeWord) : undefined + appendWholeWordsLabel: options.appendWholeWordsLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindWholeWord) : undefined, + showHistoryHint: () => showHistoryKeybindingHint(_keybindingService) }, contextKeyService, options.showOptionButtons)); - // Find History with update delayer this._updateHistoryDelayer = new Delayer<void>(500); @@ -221,7 +223,7 @@ export abstract class SimpleFindWidget extends Widget { } private _getKeybinding(actionId: string): string { - let kb = this._keybindingService?.lookupKeybinding(actionId); + const kb = this._keybindingService?.lookupKeybinding(actionId); if (!kb) { return ''; } @@ -328,7 +330,7 @@ export abstract class SimpleFindWidget extends Widget { this._matchesCount.innerText = ''; let label = ''; this._matchesCount.classList.toggle('no-results', false); - if (count?.resultCount && count?.resultCount <= 0) { + if (count?.resultCount !== undefined && count?.resultCount === 0) { label = NLS_NO_RESULTS; if (!!this.inputValue) { this._matchesCount.classList.toggle('no-results', true); @@ -336,10 +338,24 @@ export abstract class SimpleFindWidget extends Widget { } else if (count?.resultCount) { label = strings.format(NLS_MATCHES_LOCATION, count.resultIndex + 1, count?.resultCount); } + alertFn(this._announceSearchResults(label, this.inputValue)); this._matchesCount.appendChild(document.createTextNode(label)); this._findInput?.domNode.insertAdjacentElement('afterend', this._matchesCount); this._foundMatch = !!count && count.resultCount > 0; } + + private _announceSearchResults(label: string, searchString?: string): string { + if (!searchString) { + return nls.localize('ariaSearchNoInput', "Enter search input"); + } + if (label === NLS_NO_RESULTS) { + return searchString === '' + ? nls.localize('ariaSearchNoResultEmpty', "{0} found", label) + : nls.localize('ariaSearchNoResult', "{0} found for '{1}'", label, searchString); + } + + return nls.localize('ariaSearchNoResultWithLineNumNoCurrentMatch', "{0} found for '{1}'", label, searchString); + } } // theming diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index e3111f08d6c..9a655b6983a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -16,7 +16,8 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { FontStyle, StandardTokenType, TokenMetadata, SemanticTokensLegend, SemanticTokens, ColorId } from 'vs/editor/common/languages'; +import { SemanticTokensLegend, SemanticTokens } from 'vs/editor/common/languages'; +import { FontStyle, ColorId, StandardTokenType, TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -124,7 +125,7 @@ class InspectEditorTokens extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - let controller = InspectEditorTokensController.get(editor); + const controller = InspectEditorTokensController.get(editor); if (controller) { controller.toggle(); } @@ -161,7 +162,7 @@ function renderTokenText(tokenText: string): string { } let result: string = ''; for (let charIndex = 0, len = tokenText.length; charIndex < len; charIndex++) { - let charCode = tokenText.charCodeAt(charIndex); + const charCode = tokenText.charCodeAt(charIndex); switch (charCode) { case CharCode.Tab: result += '\u2192'; // → @@ -279,8 +280,8 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return; } - let tmMetadata = textMateTokenInfo?.metadata; - let semMetadata = semanticTokenInfo?.metadata; + const tmMetadata = textMateTokenInfo?.metadata; + const semMetadata = semanticTokenInfo?.metadata; const semTokenText = semanticTokenInfo && renderTokenText(this._model.getValueInRange(semanticTokenInfo.range)); const tmTokenText = textMateTokenInfo && renderTokenText(this._model.getLineContent(position.lineNumber).substring(textMateTokenInfo.token.startIndex, textMateTokenInfo.token.endIndex)); @@ -326,7 +327,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const propertiesByDefValue: { [rule: string]: string[] } = {}; const allDefValues = new Array<[Array<HTMLElement | string>, string]>(); // remember the order // first collect to detect when the same rule is used for multiple properties - for (let property of properties) { + for (const property of properties) { if (semanticTokenInfo.metadata[property] !== undefined) { const definition = semanticTokenInfo.definitions[property]; const defValue = this._renderTokenStyleDefinition(definition, property); @@ -349,7 +350,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { } if (textMateTokenInfo) { - let theme = this._themeService.getColorTheme(); + const theme = this._themeService.getColorTheme(); dom.append(this._domNode, $('hr.tiw-metadata-separator')); const table = dom.append(this._domNode, $('table.tiw-metadata-table')); const tbody = dom.append(table, $('tbody')); @@ -372,7 +373,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { $('td.tiw-metadata-value.tiw-metadata-scopes', undefined, ...scopes), )); - let matchingRule = findMatchingThemeRule(theme, textMateTokenInfo.token.scopes, false); + const matchingRule = findMatchingThemeRule(theme, textMateTokenInfo.token.scopes, false); const semForeground = semanticTokenInfo?.metadata?.foreground; if (matchingRule) { if (semForeground !== textMateTokenInfo.metadata.foreground) { @@ -399,7 +400,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const elements = new Array<HTMLElement | string>(); function render(property: 'foreground' | 'background') { - let value = semantic?.[property] || tm?.[property]; + const value = semantic?.[property] || tm?.[property]; if (value !== undefined) { const semanticStyle = semantic?.[property] ? 'tiw-metadata-semantic' : ''; elements.push($('tr', undefined, @@ -457,12 +458,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { } private _decodeMetadata(metadata: number): IDecodedMetadata { - let colorMap = this._themeService.getColorTheme().tokenColorMap; - let languageId = TokenMetadata.getLanguageId(metadata); - let tokenType = TokenMetadata.getTokenType(metadata); - let fontStyle = TokenMetadata.getFontStyle(metadata); - let foreground = TokenMetadata.getForeground(metadata); - let background = TokenMetadata.getBackground(metadata); + const colorMap = this._themeService.getColorTheme().tokenColorMap; + const languageId = TokenMetadata.getLanguageId(metadata); + const tokenType = TokenMetadata.getTokenType(metadata); + const fontStyle = TokenMetadata.getFontStyle(metadata); + const foreground = TokenMetadata.getForeground(metadata); + const background = TokenMetadata.getBackground(metadata); return { languageId: this._languageService.languageIdCodec.decodeLanguageId(languageId), tokenType: tokenType, @@ -487,14 +488,14 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { private _getTokensAtPosition(grammar: IGrammar, position: Position): ITextMateTokenInfo { const lineNumber = position.lineNumber; - let stateBeforeLine = this._getStateBeforeLine(grammar, lineNumber); + const stateBeforeLine = this._getStateBeforeLine(grammar, lineNumber); - let tokenizationResult1 = grammar.tokenizeLine(this._model.getLineContent(lineNumber), stateBeforeLine); - let tokenizationResult2 = grammar.tokenizeLine2(this._model.getLineContent(lineNumber), stateBeforeLine); + const tokenizationResult1 = grammar.tokenizeLine(this._model.getLineContent(lineNumber), stateBeforeLine); + const tokenizationResult2 = grammar.tokenizeLine2(this._model.getLineContent(lineNumber), stateBeforeLine); let token1Index = 0; for (let i = tokenizationResult1.tokens.length - 1; i >= 0; i--) { - let t = tokenizationResult1.tokens[i]; + const t = tokenizationResult1.tokens[i]; if (position.column - 1 >= t.startIndex) { token1Index = i; break; @@ -519,7 +520,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { let state: StackElement | null = null; for (let i = 1; i < lineNumber; i++) { - let tokenizationResult = grammar.tokenizeLine(this._model.getLineContent(i), state); + const tokenizationResult = grammar.tokenizeLine(this._model.getLineContent(i), state); state = tokenizationResult.ruleStack; } @@ -622,7 +623,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const scopes = $('ul.tiw-metadata-values'); const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope : [String(matchingRule.scope)]; - for (let strScope of strScopes) { + for (const strScope of strScopes) { scopes.appendChild($('li.tiw-metadata-value.tiw-metadata-scopes', undefined, strScope)); } @@ -675,7 +676,7 @@ registerEditorAction(InspectEditorTokens); registerThemingParticipant((theme, collector) => { const border = theme.getColor(editorHoverBorder); if (border) { - let borderWidth = isHighContrast(theme.type) ? 2 : 1; + const borderWidth = isHighContrast(theme.type) ? 2 : 1; collector.addRule(`.monaco-editor .token-inspect-widget { border: ${borderWidth}px solid ${border}; }`); collector.addRule(`.monaco-editor .token-inspect-widget .tiw-metadata-separator { background-color: ${border}; }`); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts index 53dc09e2826..bbdbbd99349 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts @@ -821,5 +821,5 @@ const schema: IJSONSchema = { } }; -let schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution); +const schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution); schemaRegistry.registerSchema(schemaId, schema); diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index e0fffb28ca8..922bf591bf7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -63,10 +63,10 @@ class DocumentSymbolBreadcrumbsSource implements IBreadcrumbsDataSource<Document if (!item) { return []; } - let chain: Array<OutlineGroup | OutlineElement> = []; + const chain: Array<OutlineGroup | OutlineElement> = []; while (item) { chain.push(item); - let parent: any = item.parent; + const parent: any = item.parent; if (parent instanceof OutlineModel) { break; } @@ -75,9 +75,9 @@ class DocumentSymbolBreadcrumbsSource implements IBreadcrumbsDataSource<Document } item = parent; } - let result: Array<OutlineGroup | OutlineElement> = []; + const result: Array<OutlineGroup | OutlineElement> = []; for (let i = chain.length - 1; i >= 0; i--) { - let element = chain[i]; + const element = chain[i]; if (this._isFiltered(element)) { break; } @@ -282,7 +282,7 @@ class DocumentSymbolsOutline implements IOutline<DocumentSymbolItem> { this._outlineDisposables.add(toDisposable(() => cts.dispose(true))); try { - let model = await this._outlineModelService.getOrCreate(buffer, cts.token); + const model = await this._outlineModelService.getOrCreate(buffer, cts.token); if (cts.token.isCancellationRequested) { // cancelled -> do nothing return; diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index cec3c9c01f8..2f406a83c22 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -66,18 +66,13 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv } } -Registry.as<IQuickAccessRegistry>(QuickaccesExtensions.Quickaccess).registerQuickAccessProvider({ - ctor: GotoLineQuickAccessProvider, - prefix: AbstractGotoLineQuickAccessProvider.PREFIX, - placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. 42:5 for line 42 and column 5)."), - helpEntries: [{ description: localize('gotoLineQuickAccess', "Go to Line/Column"), needsEditor: true }] -}); - class GotoLineAction extends Action2 { + static readonly ID = 'workbench.action.gotoLine'; + constructor() { super({ - id: 'workbench.action.gotoLine', + id: GotoLineAction.ID, title: { value: localize('gotoLine', "Go to Line/Column..."), original: 'Go to Line/Column...' }, f1: true, keybinding: { @@ -95,3 +90,10 @@ class GotoLineAction extends Action2 { } registerAction2(GotoLineAction); + +Registry.as<IQuickAccessRegistry>(QuickaccesExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: GotoLineQuickAccessProvider, + prefix: AbstractGotoLineQuickAccessProvider.PREFIX, + placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. 42:5 for line 42 and column 5)."), + helpEntries: [{ description: localize('gotoLineQuickAccess', "Go to Line/Column"), commandId: GotoLineAction.ID }] +}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 64e87aef2fc..ac03f77fa5e 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -243,22 +243,13 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess } } -Registry.as<IQuickAccessRegistry>(QuickaccessExtensions.Quickaccess).registerQuickAccessProvider({ - ctor: GotoSymbolQuickAccessProvider, - prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, - contextKey: 'inFileSymbolsPicker', - placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."), - helpEntries: [ - { description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, needsEditor: true }, - { description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY, needsEditor: true } - ] -}); +class GotoSymbolAction extends Action2 { -registerAction2(class GotoSymbolAction extends Action2 { + static readonly ID = 'workbench.action.gotoSymbol'; constructor() { super({ - id: 'workbench.action.gotoSymbol', + id: GotoSymbolAction.ID, title: { value: localize('gotoSymbol', "Go to Symbol in Editor..."), mnemonicTitle: localize({ key: 'miGotoSymbolInEditor', comment: ['&& denotes a mnemonic'] }, "Go to &&Symbol in Editor..."), @@ -281,4 +272,17 @@ registerAction2(class GotoSymbolAction extends Action2 { run(accessor: ServicesAccessor) { accessor.get(IQuickInputService).quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX); } +} + +registerAction2(GotoSymbolAction); + +Registry.as<IQuickAccessRegistry>(QuickaccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: GotoSymbolQuickAccessProvider, + prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, + contextKey: 'inFileSymbolsPicker', + placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."), + helpEntries: [ + { description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, commandId: GotoSymbolAction.ID }, + { description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY } + ] }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index b2aeea97c69..4b966ec9bd1 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -15,8 +15,8 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; import { CodeActionTriggerType, CodeActionProvider } from 'vs/editor/common/languages'; import { getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { applyCodeAction } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; -import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types'; +import { applyCodeAction, ApplyCodeActionReason } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; +import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/browser/types'; import { formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/browser/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; @@ -204,9 +204,7 @@ export class TrimFinalNewLinesParticipant implements ITextFileSaveParticipant { model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], _edits => prevSelection); - if (editor) { - editor.setSelections(prevSelection); - } + editor?.setSelections(prevSelection); } } @@ -364,7 +362,7 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { try { for (const action of actionsToRun.validActions) { progress.report({ message: localize('codeAction.apply', "Applying code action '{0}'.", action.action.title) }); - await this.instantiationService.invokeFunction(applyCodeAction, action); + await this.instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave); } } catch { // Failure to apply a code action should not block other on save actions @@ -377,6 +375,7 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], progress: IProgress<CodeActionProvider>, token: CancellationToken) { return getCodeActions(this.languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: CodeActionTriggerType.Auto, + triggerAction: CodeActionTriggerSource.OnSave, filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, }, progress, token); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index df8047008f3..753c32d24c0 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -167,7 +167,7 @@ export class SuggestEnabledInput extends Widget implements IThemable { this._register(this.inputWidget.onDidFocusEditorText(() => this._onDidFocus.fire())); this._register(this.inputWidget.onDidBlurEditorText(() => this._onDidBlur.fire())); - let scopeHandle = uri.parse(resourceHandle); + const scopeHandle = uri.parse(resourceHandle); this.inputModel = modelService.createModel('', null, scopeHandle, true); this._register(this.inputModel); this.inputWidget.setModel(this.inputModel); @@ -191,7 +191,7 @@ export class SuggestEnabledInput extends Widget implements IThemable { const inputWidgetModel = this.inputWidget.getModel(); if (inputWidgetModel) { this._register(inputWidgetModel.onDidChangeContent(() => { - let content = this.getValue(); + const content = this.getValue(); this.placeholderText.style.visibility = content ? 'hidden' : 'visible'; if (preexistingContent.trim() === content.trim()) { return; } this._onInputDidChange.fire(undefined); @@ -199,7 +199,7 @@ export class SuggestEnabledInput extends Widget implements IThemable { })); } - let validatedSuggestProvider = { + const validatedSuggestProvider = { provideResults: suggestionProvider.provideResults, sortKey: suggestionProvider.sortKey || (a => a), triggerCharacters: suggestionProvider.triggerCharacters || [] @@ -210,12 +210,12 @@ export class SuggestEnabledInput extends Widget implements IThemable { this._register(languageFeaturesService.completionProvider.register({ scheme: scopeHandle.scheme, pattern: '**/' + scopeHandle.path, hasAccessToAllModels: true }, { triggerCharacters: validatedSuggestProvider.triggerCharacters, provideCompletionItems: (model: ITextModel, position: Position, _context: languages.CompletionContext) => { - let query = model.getValue(); + const query = model.getValue(); const zeroIndexedColumn = position.column - 1; - let zeroIndexedWordStart = query.lastIndexOf(' ', zeroIndexedColumn - 1) + 1; - let alreadyTypedCount = zeroIndexedColumn - zeroIndexedWordStart; + const zeroIndexedWordStart = query.lastIndexOf(' ', zeroIndexedColumn - 1) + 1; + const alreadyTypedCount = zeroIndexedColumn - zeroIndexedWordStart; // dont show suggestions if the user has typed something, but hasn't used the trigger character if (alreadyTypedCount > 0 && validatedSuggestProvider.triggerCharacters.indexOf(query[zeroIndexedWordStart]) === -1) { diff --git a/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts b/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts index 7e1393bfc9e..b0c66b9acb9 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts @@ -37,11 +37,11 @@ export class SelectionClipboard extends Disposable implements IEditorContributio } })); - let setSelectionToClipboard = this._register(new RunOnceScheduler(() => { + const setSelectionToClipboard = this._register(new RunOnceScheduler(() => { if (!editor.hasModel()) { return; } - let model = editor.getModel(); + const model = editor.getModel(); let selections = editor.getSelections(); selections = selections.slice(0); selections.sort(Range.compareRangesUsingStarts); @@ -61,12 +61,12 @@ export class SelectionClipboard extends Disposable implements IEditorContributio return; } - let result: string[] = []; + const result: string[] = []; for (const sel of selections) { result.push(model.getValueInRange(sel, EndOfLinePreference.TextDefined)); } - let textToCopy = result.join(model.getEOL()); + const textToCopy = result.join(model.getEOL()); clipboardService.writeText(textToCopy, 'selection'); }, 100)); diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index b0fa1995753..9c2bf728cef 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -112,11 +112,11 @@ suite('Save Participants', function () { const textContent = 'Trim New Line'; // single line - let lineContent = `${textContent}`; + const lineContent = `${textContent}`; model.textEditorModel.setValue(lineContent); // apply edits and push to undo stack. - let textEdits = [{ range: new Range(1, 14, 1, 14), text: '.', forceMoveMarkers: false }]; + const textEdits = [{ range: new Range(1, 14, 1, 14), text: '.', forceMoveMarkers: false }]; model.textEditorModel.pushEditOperations([new Selection(1, 14, 1, 14)], textEdits, () => { return [new Selection(1, 15, 1, 15)]; }); // undo @@ -138,7 +138,7 @@ suite('Save Participants', function () { const participant = new TrimFinalNewLinesParticipant(configService, undefined!); const textContent = 'Test'; const eol = `${model.textEditorModel.getEOL()}`; - let content = `${textContent}${eol}${eol}`; + const content = `${textContent}${eol}${eol}`; model.textEditorModel.setValue(content); // save many times @@ -164,7 +164,7 @@ suite('Save Participants', function () { configService.setUserConfiguration('files', { 'trimTrailingWhitespace': true }); const participant = new TrimWhitespaceParticipant(configService, undefined!); const textContent = 'Test'; - let content = `${textContent} `; + const content = `${textContent} `; model.textEditorModel.setValue(content); // save many times diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts index 7e74b3ec1fc..2883aa76346 100644 --- a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -49,7 +49,7 @@ export class CommentFormActions implements IDisposable { triggerDefaultAction() { if (this._actions.length) { - let lastAction = this._actions[0]; + const lastAction = this._actions[0]; if (lastAction.enabled) { this.actionHandler(lastAction); diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 563ce8a4c9e..4c2b70d824b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -46,7 +46,7 @@ export class CommentGlyphWidget { setLineNumber(lineNumber: number): void { this._lineNumber = lineNumber; - let commentsDecorations = [{ + const commentsDecorations = [{ range: { startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 1 diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index db768308374..d1d118f5c15 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -224,14 +224,14 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { private createActionsToolbar() { const actions: IAction[] = []; - let hasReactionHandler = this.commentService.hasReactionHandler(this.owner); + const hasReactionHandler = this.commentService.hasReactionHandler(this.owner); if (hasReactionHandler) { - let toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []); + const toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []); actions.push(toggleReactionAction); } - let commentMenus = this.commentService.getCommentMenus(this.owner); + const commentMenus = this.commentService.getCommentMenus(this.owner); const menu = commentMenus.getCommentTitleActions(this.comment, this._contextKeyService); this._register(menu); this._register(menu.onDidChange(e => { @@ -261,21 +261,26 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { } if (action.id === ReactionAction.ID) { - let item = new ReactionActionViewItem(action); + const item = new ReactionActionViewItem(action); return item; } else if (action instanceof MenuItemAction) { return this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined); } else if (action instanceof SubmenuItemAction) { return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, undefined); } else { - let item = new ActionViewItem({}, action, options); + const item = new ActionViewItem({}, action, options); return item; } } + async submitComment(): Promise<void> { + if (this._commentEditor && this._commentFormActions) { + this._commentFormActions.triggerDefaultAction(); + } + } + private createReactionPicker(reactionGroup: languages.CommentReaction[]): ToggleReactionsAction { - let toggleReactionActionViewItem: DropdownMenuActionViewItem; - let toggleReactionAction = this._register(new ToggleReactionsAction(() => { + const toggleReactionAction = this._register(new ToggleReactionsAction(() => { if (toggleReactionActionViewItem) { toggleReactionActionViewItem.show(); } @@ -299,7 +304,7 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { toggleReactionAction.menuActions = reactionMenuActions; - toggleReactionActionViewItem = new DropdownMenuActionViewItem( + const toggleReactionActionViewItem: DropdownMenuActionViewItem = new DropdownMenuActionViewItem( toggleReactionAction, (<ToggleReactionsAction>toggleReactionAction).menuActions, this.contextMenuService, @@ -341,9 +346,9 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { }); this._register(this._reactionsActionBar); - let hasReactionHandler = this.commentService.hasReactionHandler(this.owner); + const hasReactionHandler = this.commentService.hasReactionHandler(this.owner); this.comment.commentReactions!.filter(reaction => !!reaction.count).map(reaction => { - let action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => { + const action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => { try { await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction); } catch (e) { @@ -362,13 +367,11 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { } }, reaction.iconPath, reaction.count); - if (this._reactionsActionBar) { - this._reactionsActionBar.push(action, { label: true, icon: true }); - } + this._reactionsActionBar?.push(action, { label: true, icon: true }); }); if (hasReactionHandler) { - let toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []); + const toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []); this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); } } @@ -397,7 +400,7 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { const lastColumn = this._commentEditorModel.getLineContent(lastLine).length + 1; this._commentEditor.setSelection(new Selection(lastLine, lastColumn, lastLine, lastColumn)); - let commentThread = this.commentThread; + const commentThread = this.commentThread; commentThread.input = { uri: this._commentEditor.getModel()!.uri, value: this.commentBodyValue @@ -414,9 +417,9 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => { if (commentThread.input && this._commentEditor && this._commentEditor.getModel()!.uri === commentThread.input.uri) { - let newVal = this._commentEditor.getValue(); + const newVal = this._commentEditor.getValue(); if (newVal !== commentThread.input.value) { - let input = commentThread.input; + const input = commentThread.input; input.value = newVal; commentThread.input = input; this.commentService.setActiveCommentThread(commentThread); @@ -469,13 +472,11 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { this._register(menu); this._register(menu.onDidChange(() => { - if (this._commentFormActions) { - this._commentFormActions.setActions(menu); - } + this._commentFormActions?.setActions(menu); })); this._commentFormActions = new CommentFormActions(formActions, (action: IAction): void => { - let text = this._commentEditor!.getValue(); + const text = this._commentEditor!.getValue(); action.run({ thread: this.commentThread, @@ -579,7 +580,7 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { } function fillInActions(groups: [string, Array<MenuItemAction | SubmenuItemAction>][], target: IAction[] | { primary: IAction[]; secondary: IAction[] }, useAlternativeActions: boolean, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void { - for (let tuple of groups) { + for (const tuple of groups) { let [group, actions] = tuple; if (useAlternativeActions) { actions = actions.map(a => (a instanceof MenuItemAction) && !!a.alt ? a.alt : a); diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 2c2c9e13537..0905db307c3 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -74,7 +74,7 @@ export class CommentReply<T extends IRange | ICellRange> extends Disposable { }); let resource = URI.parse(`${COMMENT_SCHEME}://${this._commentThread.extensionId}/commentinput-${modeId}.md?${params}`); // TODO. Remove params once extensions adopt authority. - let commentController = this.commentService.getCommentController(owner); + const commentController = this.commentService.getCommentController(owner); if (commentController) { resource = resource.with({ authority: commentController.id }); } @@ -123,7 +123,7 @@ export class CommentReply<T extends IRange | ICellRange> extends Disposable { } public getPendingComment(): string | null { - let model = this.commentEditor.getModel(); + const model = this.commentEditor.getModel(); if (model && model.getValueLength() > 0) { // checking length is cheap return model.getValue(); @@ -191,7 +191,7 @@ export class CommentReply<T extends IRange | ICellRange> extends Disposable { } }]; - this.commentEditor.setDecorations('review-zone-widget', COMMENTEDITOR_DECORATION_KEY, decorations); + this.commentEditor.setDecorationsByType('review-zone-widget', COMMENTEDITOR_DECORATION_KEY, decorations); } } @@ -205,9 +205,9 @@ export class CommentReply<T extends IRange | ICellRange> extends Disposable { })); this._commentThreadDisposables.push(commentEditor.getModel()!.onDidChangeContent(() => { - let modelContent = commentEditor.getValue(); + const modelContent = commentEditor.getValue(); if (this._commentThread.input && this._commentThread.input.uri === commentEditor.getModel()!.uri && this._commentThread.input.value !== modelContent) { - let newInput: languages.CommentInput = this._commentThread.input; + const newInput: languages.CommentInput = this._commentThread.input; newInput.value = modelContent; this._commentThread.input = newInput; } @@ -215,7 +215,7 @@ export class CommentReply<T extends IRange | ICellRange> extends Disposable { })); this._commentThreadDisposables.push(this._commentThread.onDidChangeInput(input => { - let thread = this._commentThread; + const thread = this._commentThread; if (thread.input && thread.input.uri !== commentEditor.getModel()!.uri) { return; @@ -250,9 +250,7 @@ export class CommentReply<T extends IRange | ICellRange> extends Disposable { })); this._commentFormActions = new CommentFormActions(container, async (action: IAction) => { - if (this._actionRunDelegate) { - this._actionRunDelegate(); - } + this._actionRunDelegate?.(); action.run({ thread: this._commentThread, diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index eed900b49eb..4efc5d1a825 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -13,6 +13,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; export const ICommentService = createDecorator<ICommentService>('commentService'); @@ -72,6 +73,8 @@ export interface ICommentService { readonly onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges }>; readonly onDidSetDataProvider: Event<void>; readonly onDidDeleteDataProvider: Event<string>; + readonly onDidChangeCommentingEnabled: Event<boolean>; + readonly isCommentingEnabled: boolean; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; setWorkspaceComments(owner: string, commentsByResource: CommentThread<IRange | ICellRange>[]): void; removeWorkspaceComments(owner: string): void; @@ -92,6 +95,7 @@ export interface ICommentService { toggleReaction(owner: string, resource: URI, thread: CommentThread<IRange | ICellRange>, comment: Comment, reaction: CommentReaction): Promise<void>; setActiveCommentThread(commentThread: CommentThread<IRange | ICellRange> | null): void; setCurrentCommentThread(commentThread: CommentThread<IRange | ICellRange> | undefined): void; + enableCommenting(enable: boolean): void; } export class CommentService extends Disposable implements ICommentService { @@ -124,6 +128,9 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidChangeCurrentCommentThread = this._register(new Emitter<CommentThread | undefined>()); readonly onDidChangeCurrentCommentThread = this._onDidChangeCurrentCommentThread.event; + private readonly _onDidChangeCommentingEnabled = this._register(new Emitter<boolean>()); + readonly onDidChangeCommentingEnabled = this._onDidChangeCommentingEnabled.event; + private readonly _onDidChangeActiveCommentingRange: Emitter<{ range: Range; commentingRangesInfo: CommentingRanges; @@ -135,11 +142,27 @@ export class CommentService extends Disposable implements ICommentService { private _commentControls = new Map<string, ICommentController>(); private _commentMenus = new Map<string, CommentMenus>(); + private _isCommentingEnabled: boolean = true; constructor( - @IInstantiationService protected instantiationService: IInstantiationService + @IInstantiationService protected instantiationService: IInstantiationService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService ) { super(); + this._register(layoutService.onDidChangeZenMode(e => { + this.enableCommenting(!e); + })); + } + + get isCommentingEnabled(): boolean { + return this._isCommentingEnabled; + } + + enableCommenting(enable: boolean): void { + if (enable !== this._isCommentingEnabled) { + this._isCommentingEnabled = enable; + this._onDidChangeCommentingEnabled.fire(enable); + } } /** @@ -205,10 +228,8 @@ export class CommentService extends Disposable implements ICommentService { } disposeCommentThread(owner: string, threadId: string) { - let controller = this.getCommentController(owner); - if (controller) { - controller.deleteCommentThreadMain(threadId); - } + const controller = this.getCommentController(owner); + controller?.deleteCommentThreadMain(threadId); } getCommentMenus(owner: string): CommentMenus { @@ -216,7 +237,7 @@ export class CommentService extends Disposable implements ICommentService { return this._commentMenus.get(owner)!; } - let menu = this.instantiationService.createInstance(CommentMenus); + const menu = this.instantiationService.createInstance(CommentMenus); this._commentMenus.set(owner, menu); return menu; } @@ -256,7 +277,7 @@ export class CommentService extends Disposable implements ICommentService { } async getDocumentComments(resource: URI): Promise<(ICommentInfo | null)[]> { - let commentControlResult: Promise<ICommentInfo | null>[] = []; + const commentControlResult: Promise<ICommentInfo | null>[] = []; this._commentControls.forEach(control => { commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None) @@ -269,7 +290,7 @@ export class CommentService extends Disposable implements ICommentService { } async getNotebookComments(resource: URI): Promise<(INotebookCommentInfo | null)[]> { - let commentControlResult: Promise<INotebookCommentInfo | null>[] = []; + const commentControlResult: Promise<INotebookCommentInfo | null>[] = []; this._commentControls.forEach(control => { commentControlResult.push(control.getNotebookComments(resource, CancellationToken.None) @@ -282,13 +303,13 @@ export class CommentService extends Disposable implements ICommentService { } async getCommentingRanges(resource: URI): Promise<IRange[]> { - let commentControlResult: Promise<IRange[]>[] = []; + const commentControlResult: Promise<IRange[]>[] = []; this._commentControls.forEach(control => { commentControlResult.push(control.getCommentingRanges(resource, CancellationToken.None)); }); - let ret = await Promise.all(commentControlResult); + const ret = await Promise.all(commentControlResult); return ret.reduce((prev, curr) => { prev.push(...curr); return prev; }, []); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index 59a1d57b12b..f8c9397134f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -74,12 +74,12 @@ export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends D this._updateAriaLabel(); this._register(dom.addDisposableListener(this._commentsElement, dom.EventType.KEY_DOWN, (e) => { - let event = new StandardKeyboardEvent(e as KeyboardEvent); + const event = new StandardKeyboardEvent(e as KeyboardEvent); if (event.equals(KeyCode.UpArrow) || event.equals(KeyCode.DownArrow)) { const moveFocusWithinBounds = (change: number): number => { if (this._focusedComment === undefined && change >= 0) { return 0; } if (this._focusedComment === undefined && change < 0) { return this._commentElements.length - 1; } - let newIndex = this._focusedComment! + change; + const newIndex = this._focusedComment! + change; return Math.min(Math.max(0, newIndex), this._commentElements.length - 1); }; @@ -111,7 +111,7 @@ export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends D } private _refresh() { - let dimensions = dom.getClientArea(this.container); + const dimensions = dom.getClientArea(this.container); this._onDidResize.fire(dimensions); } @@ -126,7 +126,7 @@ export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends D } getCommentCoords(commentUniqueId: number): { thread: dom.IDomNodePagePosition; comment: dom.IDomNodePagePosition } | undefined { - let matchedNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === commentUniqueId); + const matchedNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === commentUniqueId); if (matchedNode && matchedNode.length) { const commentThreadCoords = dom.getDomNodePagePosition(this._commentElements[0].domNode); const commentCoords = dom.getDomNodePagePosition(matchedNode[0].domNode); @@ -143,11 +143,11 @@ export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends D const oldCommentsLen = this._commentElements.length; const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0; - let commentElementsToDel: CommentNode<T>[] = []; - let commentElementsToDelIndex: number[] = []; + const commentElementsToDel: CommentNode<T>[] = []; + const commentElementsToDelIndex: number[] = []; for (let i = 0; i < oldCommentsLen; i++) { - let comment = this._commentElements[i].comment; - let newComment = commentThread.comments ? commentThread.comments.filter(c => c.uniqueIdInThread === comment.uniqueIdInThread) : []; + const comment = this._commentElements[i].comment; + const newComment = commentThread.comments ? commentThread.comments.filter(c => c.uniqueIdInThread === comment.uniqueIdInThread) : []; if (newComment.length) { this._commentElements[i].update(newComment[0]); @@ -169,11 +169,11 @@ export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends D let lastCommentElement: HTMLElement | null = null; - let newCommentNodeList: CommentNode<T>[] = []; - let newCommentsInEditMode: CommentNode<T>[] = []; + const newCommentNodeList: CommentNode<T>[] = []; + const newCommentsInEditMode: CommentNode<T>[] = []; for (let i = newCommentsLen - 1; i >= 0; i--) { - let currentComment = commentThread.comments![i]; - let oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === currentComment.uniqueIdInThread); + const currentComment = commentThread.comments![i]; + const oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === currentComment.uniqueIdInThread); if (oldCommentNode.length) { lastCommentElement = oldCommentNode[0].domNode; newCommentNodeList.unshift(oldCommentNode[0]); @@ -233,7 +233,7 @@ export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends D } private createNewCommentNode(comment: languages.Comment): CommentNode<T> { - let newCommentNode = this._scopedInstatiationService.createInstance(CommentNode, + const newCommentNode = this._scopedInstatiationService.createInstance(CommentNode, this._commentThread, comment, this.owner, diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts index 1c16bb4ffdb..ec2d56e230e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -45,7 +45,7 @@ export class CommentThreadHeader<T = IRange> extends Disposable { } protected _fillHead(): void { - let titleElement = dom.append(this._headElement, dom.$('.review-title')); + const titleElement = dom.append(this._headElement, dom.$('.review-title')); this._headingLabel = dom.append(titleElement, dom.$('span.filename')); this.createThreadLabel(); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts b/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts index 5af04a9fe0c..25b62cec998 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IRange } from 'vs/editor/common/core/range'; +import { CommentThread, CommentThreadCollapsibleState } from 'vs/editor/common/languages'; import { IModelDecorationOptions, IModelDeltaDecoration } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; @@ -34,6 +35,8 @@ export class CommentThreadRangeDecorator extends Disposable { private decorationIds: string[] = []; private activeDecorationIds: string[] = []; private editor: ICodeEditor | undefined; + private threadCollapseStateListeners: IDisposable[] = []; + private currentThreadCollapseStateListener: IDisposable | undefined; constructor(commentService: ICommentService) { super(); @@ -55,19 +58,36 @@ export class CommentThreadRangeDecorator extends Disposable { this.activeDecorationOptions = ModelDecorationOptions.createDynamic(activeDecorationOptions); this._register(commentService.onDidChangeCurrentCommentThread(thread => { - if (!this.editor) { - return; - } - let newDecoration: CommentThreadRangeDecoration[] = []; - if (thread) { - const range = thread.range; - if (!((range.startLineNumber === range.endLineNumber) && (range.startColumn === range.endColumn))) { + this.updateCurrent(thread); + })); + this._register(commentService.onDidUpdateCommentThreads(() => { + this.updateCurrent(undefined); + })); + } + + private updateCurrent(thread: CommentThread<IRange> | undefined) { + if (!this.editor) { + return; + } + this.currentThreadCollapseStateListener?.dispose(); + const newDecoration: CommentThreadRangeDecoration[] = []; + if (thread) { + const range = thread.range; + if (!((range.startLineNumber === range.endLineNumber) && (range.startColumn === range.endColumn))) { + if (thread.collapsibleState === CommentThreadCollapsibleState.Expanded) { + this.currentThreadCollapseStateListener = thread.onDidChangeCollapsibleState(state => { + if (state === CommentThreadCollapsibleState.Collapsed) { + this.updateCurrent(undefined); + } + }); newDecoration.push(new CommentThreadRangeDecoration(range, this.activeDecorationOptions)); } } - this.activeDecorationIds = this.editor.deltaDecorations(this.activeDecorationIds, newDecoration); + } + this.editor.changeDecorations((changeAccessor) => { + this.activeDecorationIds = changeAccessor.deltaDecorations(this.activeDecorationIds, newDecoration); newDecoration.forEach((decoration, index) => decoration.id = this.decorationIds[index]); - })); + }); } public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) { @@ -75,6 +95,7 @@ export class CommentThreadRangeDecorator extends Disposable { if (!model) { return; } + dispose(this.threadCollapseStateListeners); this.editor = editor; const commentThreadRangeDecorations: CommentThreadRangeDecoration[] = []; @@ -83,17 +104,35 @@ export class CommentThreadRangeDecorator extends Disposable { if (thread.isDisposed) { return; } + const range = thread.range; // We only want to show a range decoration when there's the range spans either multiple lines // or, when is spans multiple characters on the sample line if ((range.startLineNumber === range.endLineNumber) && (range.startColumn === range.endColumn)) { return; } + + this.threadCollapseStateListeners.push(thread.onDidChangeCollapsibleState(() => { + this.update(editor, commentInfos); + })); + + if (thread.collapsibleState === CommentThreadCollapsibleState.Collapsed) { + return; + } + commentThreadRangeDecorations.push(new CommentThreadRangeDecoration(range, this.decorationOptions)); }); } - this.decorationIds = editor.deltaDecorations(this.decorationIds, commentThreadRangeDecorations); - commentThreadRangeDecorations.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + editor.changeDecorations((changeAccessor) => { + this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, commentThreadRangeDecorations); + commentThreadRangeDecorations.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + }); + } + + override dispose() { + dispose(this.threadCollapseStateListeners); + this.currentThreadCollapseStateListener?.dispose(); + super.dispose(); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index df84a908765..f4f972a1a00 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/review'; import * as dom from 'vs/base/browser/dom'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as languages from 'vs/editor/common/languages'; import { IMarkdownRendererOptions } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; @@ -18,7 +18,6 @@ import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentSe import { CommentThreadBody } from 'vs/workbench/contrib/comments/browser/commentThreadBody'; import { CommentThreadHeader } from 'vs/workbench/contrib/comments/browser/commentThreadHeader'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; -import { CommentNode } from 'vs/workbench/contrib/comments/common/commentModel'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { contrastBorder, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; @@ -148,7 +147,7 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends updateCommentThread(commentThread: languages.CommentThread<T>) { if (this._commentThread !== commentThread) { - this._commentThreadDisposables.forEach(disposable => disposable.dispose()); + dispose(this._commentThreadDisposables); } this._commentThread = commentThread; @@ -168,7 +167,7 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends } display(lineHeight: number) { - let headHeight = Math.ceil(lineHeight * 1.2); + const headHeight = Math.ceil(lineHeight * 1.2); this._header.updateHeight(headHeight); this._body.display(); @@ -273,7 +272,9 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends async submitComment() { const activeComment = this._body.activeComment; - if (activeComment && !(activeComment instanceof CommentNode)) { + if (activeComment) { + activeComment.submitComment(); + } else if ((this._commentReply?.getPendingComment()?.length ?? 0) > 0) { this._commentReply?.submitComment(); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index c2b4beebee5..99c4aae1ca9 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -56,6 +56,22 @@ export function parseMouseDownInfoFromEvent(e: IEditorMouseEvent) { return { lineNumber: range.startLineNumber }; } +export function isMouseUpEventDragFromMouseDown(mouseDownInfo: { lineNumber: number } | null, e: IEditorMouseEvent) { + if (!mouseDownInfo) { + return null; + } + + const { lineNumber } = mouseDownInfo; + + const range = e.target.range; + + if (!range) { + return null; + } + + return lineNumber; +} + export function isMouseUpEventMatchMouseDown(mouseDownInfo: { lineNumber: number } | null, e: IEditorMouseEvent) { if (!mouseDownInfo) { return null; @@ -161,7 +177,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } if (commentUniqueId !== undefined) { - let height = this.editor.getLayoutInfo().height; + const height = this.editor.getLayoutInfo().height; const coords = this._commentThreadWidget.getCommentCoords(commentUniqueId); if (coords) { const commentThreadCoords = coords.thread; @@ -198,7 +214,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget { actionRunner: () => { if (!this._commentThread.comments || !this._commentThread.comments.length) { - let newPosition = this.getPosition(); + const newPosition = this.getPosition(); if (newPosition) { let range: Range; @@ -342,7 +358,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } })); - this._commentThreadDisposables.push(this._commentThread.onDidChangeCollasibleState(state => { + this._commentThreadDisposables.push(this._commentThread.onDidChangeCollapsibleState(state => { if (state === languages.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { const lineNumber = this._commentThread.range.startLineNumber; @@ -388,7 +404,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return; } - let currentPosition = this.getPosition(); + const currentPosition = this.getPosition(); if (this._viewZone && currentPosition && currentPosition.lineNumber !== this._viewZone.afterLineNumber) { this._viewZone.afterLineNumber = currentPosition.lineNumber; diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 2d6e03e1c60..a0e155d0bdd 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -9,7 +9,7 @@ import { coalesce, findFirstInSorted } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/review'; import { IActiveCodeEditor, ICodeEditor, IEditorMouseEvent, isCodeEditor, isDiffEditor, IViewZone } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -31,7 +31,7 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND } from 'vs/workbench/common/theme'; import { CommentGlyphWidget, overviewRulerCommentingRangeForeground } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { isMouseUpEventMatchMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from 'vs/workbench/contrib/comments/browser/commentThreadZoneWidget'; +import { isMouseUpEventDragFromMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from 'vs/workbench/contrib/comments/browser/commentThreadZoneWidget'; import { ctxCommentEditorFocused, SimpleCommentEditor } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -186,7 +186,7 @@ class CommentingRangeDecorator { } private _doUpdate(editor: ICodeEditor, commentInfos: ICommentInfo[], emphasisLine: number = -1, selectionRange: Range | undefined = this._lastSelection) { - let model = editor.getModel(); + const model = editor.getModel(); if (!model) { return; } @@ -194,7 +194,7 @@ class CommentingRangeDecorator { // If there's still a selection, use that. emphasisLine = this._lastSelectionCursor ?? emphasisLine; - let commentingRangeDecorations: CommentingRangeDecoration[] = []; + const commentingRangeDecorations: CommentingRangeDecoration[] = []; for (const info of commentInfos) { info.commentingRanges.ranges.forEach(range => { const rangeObject = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); @@ -294,6 +294,11 @@ const ActiveCursorHasCommentingRange = new RawContextKey<boolean>('activeCursorH type: 'boolean' }); +const WorkspaceHasCommenting = new RawContextKey<boolean>('workspaceHasCommenting', false, { + description: nls.localize('hasCommentingProvider', "Whether the open workspace has either comments or commenting ranges."), + type: 'boolean' +}); + export class CommentController implements IEditorContribution { private readonly globalToDispose = new DisposableStore(); private readonly localToDispose = new DisposableStore(); @@ -310,8 +315,9 @@ export class CommentController implements IEditorContribution { private _computeCommentingRangePromise!: CancelablePromise<ICommentInfo[]> | null; private _computeCommentingRangeScheduler!: Delayer<Array<ICommentInfo | null>> | null; private _pendingCommentCache: { [key: string]: { [key: string]: string } }; - private _editorDisposables: IDisposable[] | undefined; + private _editorDisposables: IDisposable[] = []; private _activeCursorHasCommentingRange: IContextKey<boolean>; + private _workspaceHasCommenting: IContextKey<boolean>; constructor( editor: ICodeEditor, @@ -329,6 +335,7 @@ export class CommentController implements IEditorContribution { this._pendingCommentCache = {}; this._computePromise = null; this._activeCursorHasCommentingRange = ActiveCursorHasCommentingRange.bindTo(contextKeyService); + this._workspaceHasCommenting = WorkspaceHasCommenting.bindTo(contextKeyService); if (editor instanceof EmbeddedCodeEditorWidget) { return; @@ -340,7 +347,7 @@ export class CommentController implements IEditorContribution { this.globalToDispose.add(this._commentingRangeDecorator.onDidChangeDecorationsCount(count => { if (count === 0) { this.clearEditorListeners(); - } else if (!this._editorDisposables) { + } else if (this._editorDisposables.length === 0) { this.registerEditorListeners(); } })); @@ -360,6 +367,23 @@ export class CommentController implements IEditorContribution { this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null)); } })); + this.globalToDispose.add(this.commentService.onDidSetAllCommentThreads(e => { + if (e.commentThreads.length > 0) { + this._workspaceHasCommenting.set(true); + } + })); + + this.globalToDispose.add(this.commentService.onDidChangeCommentingEnabled(e => { + if (e) { + this.registerEditorListeners(); + this.beginCompute(); + } else { + this.clearEditorListeners(); + this._commentingRangeDecorator.update(this.editor, []); + this._commentThreadRangeDecorator.update(this.editor, []); + dispose(this._commentWidgets); + } + })); this.globalToDispose.add(this.editor.onDidChangeModel(e => this.onModelChanged(e))); this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {}); @@ -376,12 +400,17 @@ export class CommentController implements IEditorContribution { } private clearEditorListeners() { - this._editorDisposables?.forEach(disposable => disposable.dispose()); - this._editorDisposables = undefined; + dispose(this._editorDisposables); + this._editorDisposables = []; } private onEditorMouseMove(e: IEditorMouseEvent): void { - this._commentingRangeDecorator.updateHover(e.target.position?.lineNumber); + const position = e.target.position?.lineNumber; + if (e.event.leftButton.valueOf() && position && this.mouseDownInfo) { + this._commentingRangeDecorator.updateSelection(position, new Range(this.mouseDownInfo.lineNumber, 1, position, 1)); + } else { + this._commentingRangeDecorator.updateHover(position); + } } private onEditorChangeCursorSelection(e?: ICursorSelectionChangedEvent): void { @@ -441,8 +470,10 @@ export class CommentController implements IEditorContribution { return Promise.resolve([]); }).then(commentInfos => { - const meaningfulCommentInfos = coalesce(commentInfos); - this._commentingRangeDecorator.update(this.editor, meaningfulCommentInfos); + if (this.commentService.isCommentingEnabled) { + const meaningfulCommentInfos = coalesce(commentInfos); + this._commentingRangeDecorator.update(this.editor, meaningfulCommentInfos); + } }, (err) => { onUnexpectedError(err); return null; @@ -506,11 +537,11 @@ export class CommentController implements IEditorContribution { return 0; }); - let idx = findFirstInSorted(sortedWidgets, widget => { - let lineValueOne = reverse ? after.lineNumber : widget.commentThread.range.startLineNumber; - let lineValueTwo = reverse ? widget.commentThread.range.startLineNumber : after.lineNumber; - let columnValueOne = reverse ? after.column : widget.commentThread.range.startColumn; - let columnValueTwo = reverse ? widget.commentThread.range.startColumn : after.column; + const idx = findFirstInSorted(sortedWidgets, widget => { + const lineValueOne = reverse ? after.lineNumber : widget.commentThread.range.startLineNumber; + const lineValueTwo = reverse ? widget.commentThread.range.startLineNumber : after.lineNumber; + const columnValueOne = reverse ? after.column : widget.commentThread.range.startColumn; + const columnValueTwo = reverse ? widget.commentThread.range.startColumn : after.column; if (lineValueOne > lineValueTwo) { return true; } @@ -542,9 +573,8 @@ export class CommentController implements IEditorContribution { public dispose(): void { this.globalToDispose.dispose(); this.localToDispose.dispose(); - this._editorDisposables?.forEach(disposable => disposable.dispose()); - - this._commentWidgets.forEach(widget => widget.dispose()); + dispose(this._editorDisposables); + dispose(this._commentWidgets); this.editor = null!; // Strict null override - nulling out in dispose } @@ -556,7 +586,7 @@ export class CommentController implements IEditorContribution { this.localToDispose.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); this.localToDispose.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); - if (this._editorDisposables) { + if (this._editorDisposables.length) { this.clearEditorListeners(); this.registerEditorListeners(); } @@ -575,7 +605,7 @@ export class CommentController implements IEditorContribution { })); this.localToDispose.add(this.commentService.onDidUpdateCommentThreads(async e => { const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri; - if (!editorURI) { + if (!editorURI || !this.commentService.isCommentingEnabled) { return; } @@ -583,20 +613,20 @@ export class CommentController implements IEditorContribution { await this._computePromise; } - let commentInfo = this._commentInfos.filter(info => info.owner === e.owner); + const commentInfo = this._commentInfos.filter(info => info.owner === e.owner); if (!commentInfo || !commentInfo.length) { return; } - let added = e.added.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); - let removed = e.removed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); - let changed = e.changed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); + const added = e.added.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); + const removed = e.removed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); + const changed = e.changed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString()); removed.forEach(thread => { - let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); if (matchedZones.length) { - let matchedZone = matchedZones[0]; - let index = this._commentWidgets.indexOf(matchedZone); + const matchedZone = matchedZones[0]; + const index = this._commentWidgets.indexOf(matchedZone); this._commentWidgets.splice(index, 1); matchedZone.dispose(); } @@ -610,20 +640,20 @@ export class CommentController implements IEditorContribution { }); changed.forEach(thread => { - let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { - let matchedZone = matchedZones[0]; + const matchedZone = matchedZones[0]; matchedZone.update(thread); this.openCommentsView(thread); } }); added.forEach(thread => { - let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { return; } - let matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && (zoneWidget.commentThread as any).commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && (zoneWidget.commentThread as any).commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); if (matchedNewCommentThreadZones.length) { matchedNewCommentThreadZones[0].update(thread); @@ -666,22 +696,37 @@ export class CommentController implements IEditorContribution { } private onEditorMouseUp(e: IEditorMouseEvent): void { - const matchedLineNumber = isMouseUpEventMatchMouseDown(this.mouseDownInfo, e); + const matchedLineNumber = isMouseUpEventDragFromMouseDown(this.mouseDownInfo, e); this.mouseDownInfo = null; if (matchedLineNumber === null || !e.target.element) { return; } - - if (e.target.element.className.indexOf('comment-diff-added') >= 0) { - const lineNumber = e.target.position!.lineNumber; - // Check for selection at line number. - let range: Range = new Range(lineNumber, 1, lineNumber, 1); - const selection = this.editor.getSelection(); - if (selection && (selection.startLineNumber <= lineNumber) && (lineNumber <= selection.endLineNumber)) { - range = selection; - this.editor.setSelection(new Range(selection.endLineNumber, 1, selection.endLineNumber, 1)); + const mouseUpIsOnDecorator = (e.target.element.className.indexOf('comment-diff-added') >= 0); + + const lineNumber = e.target.position!.lineNumber; + let range: Range | undefined; + let selection: Range | null | undefined; + // Check for drag along gutter decoration + if ((matchedLineNumber !== lineNumber)) { + if (matchedLineNumber > lineNumber) { + selection = new Range(matchedLineNumber, this.editor.getModel()!.getLineLength(matchedLineNumber) + 1, lineNumber, 1); + } else { + selection = new Range(matchedLineNumber, 1, lineNumber, this.editor.getModel()!.getLineLength(lineNumber) + 1); } + } else if (mouseUpIsOnDecorator) { + selection = this.editor.getSelection(); + } + + // Check for selection at line number. + if (selection && (selection.startLineNumber <= lineNumber) && (lineNumber <= selection.endLineNumber)) { + range = selection; + this.editor.setSelection(new Range(selection.endLineNumber, 1, selection.endLineNumber, 1)); + } else if (mouseUpIsOnDecorator) { + range = new Range(lineNumber, 1, lineNumber, 1); + } + + if (range) { this.addOrToggleCommentAtLine(range, e); } } @@ -796,7 +841,7 @@ export class CommentController implements IEditorContribution { } private setComments(commentInfos: ICommentInfo[]): void { - if (!this.editor) { + if (!this.editor || !this.commentService.isCommentingEnabled) { return; } @@ -804,6 +849,7 @@ export class CommentController implements IEditorContribution { let lineDecorationsWidth: number = this.editor.getLayoutInfo().decorationsWidth; if (this._commentInfos.some(info => Boolean(info.commentingRanges && (Array.isArray(info.commentingRanges) ? info.commentingRanges : info.commentingRanges.ranges).length))) { + this._workspaceHasCommenting.set(true); if (!this._commentingRangeSpaceReserved) { this._commentingRangeSpaceReserved = true; let extraEditorClassName: string[] = []; @@ -837,7 +883,10 @@ export class CommentController implements IEditorContribution { this.removeCommentWidgetsAndStoreCache(); this._commentInfos.forEach(info => { - let providerCacheStore = this._pendingCommentCache[info.owner]; + if (info.threads.length > 0) { + this._workspaceHasCommenting.set(true); + } + const providerCacheStore = this._pendingCommentCache[info.owner]; info.threads = info.threads.filter(thread => !thread.isDisposed); info.threads.forEach(thread => { let pendingComment: string | null = null; @@ -858,9 +907,7 @@ export class CommentController implements IEditorContribution { } public closeWidget(): void { - if (this._commentWidgets) { - this._commentWidgets.forEach(widget => widget.hide()); - } + this._commentWidgets?.forEach(widget => widget.hide()); this.editor.focus(); this.editor.revealRangeInCenter(this.editor.getSelection()!); @@ -869,10 +916,19 @@ export class CommentController implements IEditorContribution { private removeCommentWidgetsAndStoreCache() { if (this._commentWidgets) { this._commentWidgets.forEach(zone => { - let pendingComment = zone.getPendingComment(); - let providerCacheStore = this._pendingCommentCache[zone.owner]; - - if (pendingComment) { + const pendingComment = zone.getPendingComment(); + const providerCacheStore = this._pendingCommentCache[zone.owner]; + + let lastCommentBody; + if (zone.commentThread.comments && zone.commentThread.comments.length) { + const lastComment = zone.commentThread.comments[zone.commentThread.comments.length - 1]; + if (typeof lastComment.body === 'string') { + lastCommentBody = lastComment.body; + } else { + lastCommentBody = lastComment.body.value; + } + } + if (pendingComment && (pendingComment !== lastCommentBody)) { if (!providerCacheStore) { this._pendingCommentCache[zone.owner] = {}; } @@ -890,10 +946,6 @@ export class CommentController implements IEditorContribution { this._commentWidgets = []; } - - public hasComments(): boolean { - return !!this._commentWidgets.length; - } } export class NextCommentThreadAction extends EditorAction { @@ -912,7 +964,7 @@ export class NextCommentThreadAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - let controller = CommentController.get(editor); + const controller = CommentController.get(editor); if (controller) { controller.nextCommentThread(); } @@ -935,7 +987,7 @@ export class PreviousCommentThreadAction extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - let controller = CommentController.get(editor); + const controller = CommentController.get(editor); if (controller) { controller.previousCommentThread(); } @@ -947,6 +999,25 @@ registerEditorContribution(ID, CommentController); registerEditorAction(NextCommentThreadAction); registerEditorAction(PreviousCommentThreadAction); +const TOGGLE_COMMENTING_COMMAND = 'workbench.action.toggleCommenting'; +CommandsRegistry.registerCommand({ + id: TOGGLE_COMMENTING_COMMAND, + handler: (accessor) => { + const commentService = accessor.get(ICommentService); + const enable = commentService.isCommentingEnabled; + commentService.enableCommenting(!enable); + } +}); + +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: TOGGLE_COMMENTING_COMMAND, + title: nls.localize('comments.toggleCommenting', "Toggle Editor Commenting"), + category: 'Comments', + }, + when: WorkspaceHasCommenting +}); + const ADD_COMMENT_COMMAND = 'workbench.action.addComment'; CommandsRegistry.registerCommand({ id: ADD_COMMENT_COMMAND, diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 7077d35338b..d6883919fa6 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -68,7 +68,7 @@ export class CommentsPanel extends ViewPane { container.classList.add('comments-panel'); - let domContainer = dom.append(container, dom.$('.comments-panel-container')); + const domContainer = dom.append(container, dom.$('.comments-panel-container')); this.treeContainer = dom.append(domContainer, dom.$('.tree-container')); this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); this.commentsModel = new CommentsModel(); @@ -215,12 +215,16 @@ export class CommentsPanel extends ViewPane { return false; } + if (!this.commentService.isCommentingEnabled) { + this.commentService.enableCommenting(true); + } + const range = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].range : element.range; const activeEditor = this.editorService.activeTextEditorControl; // If the active editor is a diff editor where one of the sides has the comment, // then we try to reveal the comment in the diff editor. - let currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()] + const currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()] : (activeEditor ? [activeEditor] : []); for (const editor of currentActiveResources) { diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index c641f752f30..e7c5c1650ec 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -114,7 +114,7 @@ } .review-widget .body .review-comment .review-comment-contents .comment-body .comment-body-plainstring { - white-space: pre; + white-space: pre-wrap; } .review-widget .body .review-comment .review-comment-contents .comment-body { diff --git a/src/vs/workbench/contrib/comments/browser/reactionsAction.ts b/src/vs/workbench/contrib/comments/browser/reactionsAction.ts index a3e86b9d645..11f33528a56 100644 --- a/src/vs/workbench/contrib/comments/browser/reactionsAction.ts +++ b/src/vs/workbench/contrib/comments/browser/reactionsAction.ts @@ -37,23 +37,23 @@ export class ReactionActionViewItem extends ActionViewItem { return; } - let action = this.getAction() as ReactionAction; + const action = this.getAction() as ReactionAction; if (action.class) { this.label.classList.add(action.class); } if (!action.icon) { - let reactionLabel = dom.append(this.label, dom.$('span.reaction-label')); + const reactionLabel = dom.append(this.label, dom.$('span.reaction-label')); reactionLabel.innerText = action.label; } else { - let reactionIcon = dom.append(this.label, dom.$('.reaction-icon')); + const reactionIcon = dom.append(this.label, dom.$('.reaction-icon')); reactionIcon.style.display = ''; - let uri = URI.revive(action.icon); + const uri = URI.revive(action.icon); reactionIcon.style.backgroundImage = dom.asCSSUrl(uri); reactionIcon.title = action.label; } if (action.count) { - let reactionCount = dom.append(this.label, dom.$('span.reaction-count')); + const reactionCount = dom.append(this.label, dom.$('span.reaction-count')); reactionCount.innerText = `${action.count}`; } } diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index 06ec27f01c3..51a8504988e 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -88,7 +88,7 @@ export class CommentsModel { public updateCommentThreads(event: ICommentThreadChangedEvent): boolean { const { owner, removed, changed, added } = event; - let threadsForOwner = this.commentThreadsMap.get(owner) || []; + const threadsForOwner = this.commentThreadsMap.get(owner) || []; removed.forEach(thread => { // Find resource that has the comment thread diff --git a/src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.ts b/src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.ts index a8e8e6f3de8..4aed7b2d974 100644 --- a/src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.ts +++ b/src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.ts @@ -93,20 +93,18 @@ export class DefaultConfigurationExportHelper { const processConfig = (config: IConfigurationNode) => { if (config.properties) { - for (let name in config.properties) { + for (const name in config.properties) { processProperty(name, config.properties[name]); } } - if (config.allOf) { - config.allOf.forEach(processConfig); - } + config.allOf?.forEach(processConfig); }; configurations.forEach(processConfig); const excludedProps = configRegistry.getExcludedConfigurationProperties(); - for (let name in excludedProps) { + for (const name in excludedProps) { processProperty(name, excludedProps[name]); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index e275174e00a..af5bc6b10aa 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -36,7 +36,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return instantiationService.invokeFunction(accessor => { // If it's an untitled file we must populate the untitledDocumentData const untitledString = accessor.get(IUntitledTextEditorService).getValue(resource); - let untitledDocumentData = untitledString ? VSBuffer.fromString(untitledString) : undefined; + const untitledDocumentData = untitledString ? VSBuffer.fromString(untitledString) : undefined; const id = generateUuid(); const webview = accessor.get(IWebviewService).createWebviewOverlay({ id, diff --git a/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts b/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts index cd3ba8d63cc..0adceb05463 100644 --- a/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts +++ b/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts @@ -28,7 +28,7 @@ export class ContributedCustomEditors extends Disposable { this._memento = new Memento(ContributedCustomEditors.CUSTOM_EDITORS_STORAGE_ID, storageService); - const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); for (const info of (mementoObject[ContributedCustomEditors.CUSTOM_EDITORS_ENTRY_ID] || []) as CustomEditorDescriptor[]) { this.add(new CustomEditorInfo(info)); } @@ -56,7 +56,7 @@ export class ContributedCustomEditors extends Disposable { } } - const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); mementoObject[ContributedCustomEditors.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._editors.values()); this._memento.saveMemento(); diff --git a/src/vs/workbench/contrib/customEditor/common/extensionPoint.ts b/src/vs/workbench/contrib/customEditor/common/extensionPoint.ts index aaec5953b9e..fb969556561 100644 --- a/src/vs/workbench/contrib/customEditor/common/extensionPoint.ts +++ b/src/vs/workbench/contrib/customEditor/common/extensionPoint.ts @@ -9,12 +9,12 @@ import { CustomEditorPriority, CustomEditorSelector } from 'vs/workbench/contrib import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { languagesExtPoint } from 'vs/workbench/services/language/common/languageService'; -namespace Fields { - export const viewType = 'viewType'; - export const displayName = 'displayName'; - export const selector = 'selector'; - export const priority = 'priority'; -} +const Fields = Object.freeze({ + viewType: 'viewType', + displayName: 'displayName', + selector: 'selector', + priority: 'priority', +}); export interface ICustomEditorsExtensionPoint { readonly [Fields.viewType]: string; diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 19b1e114ac0..cc2159aebd0 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -63,21 +63,23 @@ export function renderExpressionValue(expressionOrValue: IExpressionContainer | if (value !== Expression.DEFAULT_VALUE) { container.classList.add('error'); } - } else if ((expressionOrValue instanceof ExpressionContainer) && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) { - // value changed color has priority over other colors. - container.className = 'value changed'; - expressionOrValue.valueChanged = false; - } + } else { + if ((expressionOrValue instanceof ExpressionContainer) && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) { + // value changed color has priority over other colors. + container.className = 'value changed'; + expressionOrValue.valueChanged = false; + } - if (options.colorize && typeof expressionOrValue !== 'string') { - if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') { - container.classList.add(expressionOrValue.type); - } else if (!isNaN(+value)) { - container.classList.add('number'); - } else if (booleanRegex.test(value)) { - container.classList.add('boolean'); - } else if (stringRegex.test(value)) { - container.classList.add('string'); + if (options.colorize && typeof expressionOrValue !== 'string') { + if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') { + container.classList.add(expressionOrValue.type); + } else if (!isNaN(+value)) { + container.classList.add('number'); + } else if (booleanRegex.test(value)) { + container.classList.add('boolean'); + } else if (stringRegex.test(value)) { + container.classList.add('string'); + } } } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 943b41f8eb5..8fdc2c1f303 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -12,10 +12,10 @@ import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/action import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IContentWidget, IActiveCodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { IModelDecorationOptions, TrackedRangeStickiness, ITextModel, OverviewRulerLane, IModelDecorationOverviewRulerOptions } from 'vs/editor/common/model'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession, DebuggerUiMessage } from 'vs/workbench/contrib/debug/common/debug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { BreakpointWidget } from 'vs/workbench/contrib/debug/browser/breakpointWidget'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -36,6 +36,8 @@ import { ILabelService } from 'vs/platform/label/common/label'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { onUnexpectedError } from 'vs/base/common/errors'; import { noBreakWhitespace } from 'vs/base/common/strings'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { withNullAsUndefined } from 'vs/base/common/types'; const $ = dom.$; @@ -52,7 +54,7 @@ const breakpointHelperDecoration: IModelDecorationOptions = { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; -export function createBreakpointDecorations(model: ITextModel, breakpoints: ReadonlyArray<IBreakpoint>, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean): { range: Range; options: IModelDecorationOptions }[] { +export function createBreakpointDecorations(accessor: ServicesAccessor, model: ITextModel, breakpoints: ReadonlyArray<IBreakpoint>, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean): { range: Range; options: IModelDecorationOptions }[] { const result: { range: Range; options: IModelDecorationOptions }[] = []; breakpoints.forEach((breakpoint) => { if (breakpoint.lineNumber > model.getLineCount()) { @@ -65,7 +67,7 @@ export function createBreakpointDecorations(model: ITextModel, breakpoints: Read ); result.push({ - options: getBreakpointDecorationOptions(model, breakpoint, state, breakpointsActivated, showBreakpointsInOverviewRuler), + options: getBreakpointDecorationOptions(accessor, model, breakpoint, state, breakpointsActivated, showBreakpointsInOverviewRuler), range }); }); @@ -73,17 +75,47 @@ export function createBreakpointDecorations(model: ITextModel, breakpoints: Read return result; } -function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoint, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean): IModelDecorationOptions { - const { icon, message } = getBreakpointMessageAndIcon(state, breakpointsActivated, breakpoint, undefined); +function getBreakpointDecorationOptions(accessor: ServicesAccessor, model: ITextModel, breakpoint: IBreakpoint, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean): IModelDecorationOptions { + const debugService = accessor.get(IDebugService); + const languageService = accessor.get(ILanguageService); + const { icon, message, showAdapterUnverifiedMessage } = getBreakpointMessageAndIcon(state, breakpointsActivated, breakpoint, undefined); let glyphMarginHoverMessage: MarkdownString | undefined; + let unverifiedMessage: string | undefined; + if (showAdapterUnverifiedMessage) { + let langId: string | undefined; + unverifiedMessage = debugService.getModel().getSessions().map(s => { + const dbg = debugService.getAdapterManager().getDebugger(s.configuration.type); + const message = dbg?.uiMessages?.[DebuggerUiMessage.UnverifiedBreakpoints]; + if (message) { + if (!langId) { + // Lazily compute this, only if needed for some debug adapter + langId = withNullAsUndefined(languageService.guessLanguageIdByFilepathOrFirstLine(breakpoint.uri)); + } + return langId && dbg.interestedInLanguage(langId) ? message : undefined; + } + + return undefined; + }) + .find(messages => !!messages); + } + if (message) { + glyphMarginHoverMessage = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }); if (breakpoint.condition || breakpoint.hitCondition) { const languageId = model.getLanguageId(); - glyphMarginHoverMessage = new MarkdownString().appendCodeblock(languageId, message); + glyphMarginHoverMessage.appendCodeblock(languageId, message); + if (unverifiedMessage) { + glyphMarginHoverMessage.appendMarkdown('$(warning) ' + unverifiedMessage); + } } else { - glyphMarginHoverMessage = new MarkdownString().appendText(message); + glyphMarginHoverMessage.appendText(message); + if (unverifiedMessage) { + glyphMarginHoverMessage.appendMarkdown('\n\n$(warning) ' + unverifiedMessage); + } } + } else if (unverifiedMessage) { + glyphMarginHoverMessage = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendMarkdown(unverifiedMessage); } let overviewRulerDecoration: IModelDecorationOverviewRulerOptions | null = null; @@ -433,7 +465,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi if (decorations) { for (const { options } of decorations) { const clz = options.glyphMarginClassName; - if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-'))) { + if (clz && (!clz.includes('codicon-') || clz.includes('codicon-testing-') || clz.includes('codicon-merge-') || clz.includes('codicon-arrow-'))) { return false; } } @@ -469,32 +501,34 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const model = activeCodeEditor.getModel(); const breakpoints = this.debugService.getModel().getBreakpoints({ uri: model.uri }); const debugSettings = this.configurationService.getValue<IDebugConfiguration>('debug'); - const desiredBreakpointDecorations = createBreakpointDecorations(model, breakpoints, this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), debugSettings.showBreakpointsInOverviewRuler); + const desiredBreakpointDecorations = this.instantiationService.invokeFunction(accessor => createBreakpointDecorations(accessor, model, breakpoints, this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), debugSettings.showBreakpointsInOverviewRuler)); try { this.ignoreDecorationsChangedEvent = true; // Set breakpoint decorations - const decorationIds = activeCodeEditor.deltaDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId), desiredBreakpointDecorations); - this.breakpointDecorations.forEach(bpd => { - if (bpd.inlineWidget) { - bpd.inlineWidget.dispose(); - } - }); - this.breakpointDecorations = decorationIds.map((decorationId, index) => { - let inlineWidget: InlineBreakpointWidget | undefined = undefined; - const breakpoint = breakpoints[index]; - if (desiredBreakpointDecorations[index].options.before) { - const contextMenuActions = () => this.getContextMenuActions([breakpoint], activeCodeEditor.getModel().uri, breakpoint.lineNumber, breakpoint.column); - inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, desiredBreakpointDecorations[index].options.glyphMarginClassName, breakpoint, this.debugService, this.contextMenuService, contextMenuActions); - } + activeCodeEditor.changeDecorations((changeAccessor) => { + const decorationIds = changeAccessor.deltaDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId), desiredBreakpointDecorations); + this.breakpointDecorations.forEach(bpd => { + if (bpd.inlineWidget) { + bpd.inlineWidget.dispose(); + } + }); + this.breakpointDecorations = decorationIds.map((decorationId, index) => { + let inlineWidget: InlineBreakpointWidget | undefined = undefined; + const breakpoint = breakpoints[index]; + if (desiredBreakpointDecorations[index].options.before) { + const contextMenuActions = () => this.getContextMenuActions([breakpoint], activeCodeEditor.getModel().uri, breakpoint.lineNumber, breakpoint.column); + inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, desiredBreakpointDecorations[index].options.glyphMarginClassName, breakpoint, this.debugService, this.contextMenuService, contextMenuActions); + } - return { - decorationId, - breakpoint, - range: desiredBreakpointDecorations[index].range, - inlineWidget - }; + return { + decorationId, + breakpoint, + range: desiredBreakpointDecorations[index].range, + inlineWidget + }; + }); }); } finally { this.ignoreDecorationsChangedEvent = false; @@ -503,23 +537,25 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi // Set breakpoint candidate decorations const session = this.debugService.getViewModel().focusedSession; const desiredCandidateDecorations = debugSettings.showInlineBreakpointCandidates && session ? await createCandidateDecorations(this.editor.getModel(), this.breakpointDecorations, session) : []; - const candidateDecorationIds = this.editor.deltaDecorations(this.candidateDecorations.map(c => c.decorationId), desiredCandidateDecorations); - this.candidateDecorations.forEach(candidate => { - candidate.inlineWidget.dispose(); - }); - this.candidateDecorations = candidateDecorationIds.map((decorationId, index) => { - const candidate = desiredCandidateDecorations[index]; - // Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there - // In practice this happens for the first breakpoint that was set on a line - // We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information - const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.breakpoint.disabled; - const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn); - const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, ThemeIcon.asClassName(icon), candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions); - - return { - decorationId, - inlineWidget - }; + this.editor.changeDecorations((changeAccessor) => { + const candidateDecorationIds = changeAccessor.deltaDecorations(this.candidateDecorations.map(c => c.decorationId), desiredCandidateDecorations); + this.candidateDecorations.forEach(candidate => { + candidate.inlineWidget.dispose(); + }); + this.candidateDecorations = candidateDecorationIds.map((decorationId, index) => { + const candidate = desiredCandidateDecorations[index]; + // Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there + // In practice this happens for the first breakpoint that was set on a line + // We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information + const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.breakpoint.disabled; + const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn); + const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, ThemeIcon.asClassName(icon), candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions); + + return { + decorationId, + inlineWidget + }; + }); }); for (const d of this.breakpointDecorations) { @@ -599,7 +635,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi if (this.breakpointWidget) { this.breakpointWidget.dispose(); } - this.editor.deltaDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId), []); + this.editor.removeDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId)); dispose(this.toDispose); } } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 3df0cf71a74..fb873a5fc69 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -238,7 +238,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi const setDecorations = () => { const value = this.input.getModel().getValue(); const decorations = !!value ? [] : createDecorations(this.themeService.getColorTheme(), this.placeholder); - this.input.setDecorations('breakpoint-widget', DECORATION_KEY, decorations); + this.input.setDecorationsByType('breakpoint-widget', DECORATION_KEY, decorations); }; this.input.getModel().onDidChangeContent(() => setDecorations()); this.themeService.onDidColorThemeChange(() => setDecorations()); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index b255d590c9e..8772e62d4f9 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -3,48 +3,53 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as resources from 'vs/base/common/resources'; import * as dom from 'vs/base/browser/dom'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Gesture } from 'vs/base/browser/touch'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IListContextMenuEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { Action, IAction } from 'vs/base/common/actions'; -import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, IDebugModel, IDataBreakpoint, BREAKPOINTS_VIEW_ID, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, IBaseBreakpoint, IBreakpointEditorContribution, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_INPUT_FOCUSED, IInstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; -import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, DataBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { equals } from 'vs/base/common/arrays'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import * as resources from 'vs/base/common/resources'; +import { Constants } from 'vs/base/common/uint'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { localize } from 'vs/nls'; +import { createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { Constants } from 'vs/base/common/uint'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IListVirtualDelegate, IListContextMenuEvent, IListRenderer } from 'vs/base/browser/ui/list/list'; -import { IEditorPane } from 'vs/workbench/common/editor'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { Gesture } from 'vs/base/browser/touch'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IEditorPane } from 'vs/workbench/common/editor'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { registerAction2, Action2, MenuId, IMenu, IMenuService } from 'vs/platform/actions/common/actions'; -import { localize } from 'vs/nls'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { createAndFillInContextMenuActions, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Codicon } from 'vs/base/common/codicons'; -import { equals } from 'vs/base/common/arrays'; -import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; +import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DebuggerUiMessage, DEBUG_SCHEME, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; +import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; const $ = dom.$; @@ -82,6 +87,9 @@ export class BreakpointsView extends ViewPane { breakpointInputFocused: IContextKey<boolean>; private autoFocusedIndex = -1; + private hintContainer: IconLabel | undefined; + private hintDelayer: RunOnceScheduler; + constructor( options: IViewletViewOptions, @IContextMenuService contextMenuService: IContextMenuService, @@ -97,7 +105,9 @@ export class BreakpointsView extends ViewPane { @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, @ILabelService private readonly labelService: ILabelService, - @IMenuService menuService: IMenuService + @IMenuService menuService: IMenuService, + @IHoverService private readonly hoverService: IHoverService, + @ILanguageService private readonly languageService: ILanguageService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -107,7 +117,9 @@ export class BreakpointsView extends ViewPane { this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); + this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateBreakpointsHint())); this._register(this.debugService.onDidChangeState(() => this.onStateChange())); + this.hintDelayer = this._register(new RunOnceScheduler(() => this.updateBreakpointsHint(true), 4000)); } override renderBody(container: HTMLElement): void { @@ -194,6 +206,19 @@ export class BreakpointsView extends ViewPane { })); } + protected override renderHeaderTitle(container: HTMLElement, title: string): void { + super.renderHeaderTitle(container, title); + + const iconLabelContainer = dom.append(container, $('span.breakpoint-warning')); + this.hintContainer = this._register(new IconLabel(iconLabelContainer, { + supportIcons: true, hoverDelegate: { + showHover: (options, focus?) => this.hoverService.showHover({ content: options.content, target: this.hintContainer!.element }, focus), + delay: <number>this.configurationService.getValue('workbench.hover.delay') + } + })); + dom.hide(this.hintContainer.element); + } + override focus(): void { super.focus(); if (this.list) { @@ -217,9 +242,7 @@ export class BreakpointsView extends ViewPane { } super.layoutBody(height, width); - if (this.list) { - this.list.layout(height, width); - } + this.list?.layout(height, width); try { this.ignoreLayout = true; this.updateSize(); @@ -257,6 +280,36 @@ export class BreakpointsView extends ViewPane { this.maximumBodySize = this.orientation === Orientation.VERTICAL && containerModel.visibleViewDescriptors.length > 1 ? getExpandedBodySize(this.debugService.getModel(), Number.POSITIVE_INFINITY) : Number.POSITIVE_INFINITY; } + private updateBreakpointsHint(delayed = false): void { + if (!this.hintContainer) { + return; + } + + const currentType = this.debugService.getViewModel().focusedSession?.configuration.type; + const dbg = currentType ? this.debugService.getAdapterManager().getDebugger(currentType) : undefined; + const message = dbg?.uiMessages && dbg.uiMessages[DebuggerUiMessage.UnverifiedBreakpoints]; + const debuggerHasUnverifiedBps = message && this.debugService.getModel().getBreakpoints().filter(bp => { + if (bp.verified) { + return false; + } + + const langId = this.languageService.guessLanguageIdByFilepathOrFirstLine(bp.uri); + return langId && dbg.interestedInLanguage(langId); + }); + + if (message && debuggerHasUnverifiedBps?.length) { + if (delayed) { + const mdown = new MarkdownString(undefined, { isTrusted: true }).appendMarkdown(message); + this.hintContainer.setLabel('$(warning)', undefined, { title: { markdown: mdown, markdownNotSupportedFallback: message } }); + dom.show(this.hintContainer.element); + } else { + this.hintDelayer.schedule(); + } + } else { + dom.hide(this.hintContainer.element); + } + } + private onBreakpointsChange(): void { if (this.isBodyVisible()) { this.updateSize(); @@ -270,6 +323,7 @@ export class BreakpointsView extends ViewPane { this.list.focusNth(Math.min(lastFocusIndex, this.list.length - 1)); } } + this.updateBreakpointsHint(); } else { this.needsRefresh = true; } @@ -304,6 +358,7 @@ export class BreakpointsView extends ViewPane { } this.autoFocusedIndex = -1; } + this.updateBreakpointsHint(); } else { this.needsStateChange = true; } @@ -1027,7 +1082,7 @@ export function openBreakpointSource(breakpoint: IBreakpoint, sideBySide: boolea }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); } -export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: boolean, breakpoint: BreakpointItem, labelService?: ILabelService): { message?: string; icon: ThemeIcon } { +export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: boolean, breakpoint: BreakpointItem, labelService?: ILabelService): { message?: string; icon: ThemeIcon; showAdapterUnverifiedMessage?: boolean } { const debugActive = state === State.Running || state === State.Stopped; const breakpointIcon = breakpoint instanceof DataBreakpoint ? icons.dataBreakpoint : breakpoint instanceof FunctionBreakpoint ? icons.functionBreakpoint : breakpoint.logMessage ? icons.logBreakpoint : icons.breakpoint; @@ -1046,6 +1101,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: return { icon: breakpointIcon.unverified, message: ('message' in breakpoint && breakpoint.message) ? breakpoint.message : (breakpoint.logMessage ? localize('unverifiedLogpoint', "Unverified Logpoint") : localize('unverifiedBreakpoint', "Unverified Breakpoint")), + showAdapterUnverifiedMessage: true }; } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index d595b7768d1..11584519c47 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -16,11 +16,11 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView' import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, } from 'vs/workbench/contrib/debug/common/debug'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService'; -import { ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SET_EXPRESSION_COMMAND_ID, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SET_EXPRESSION_COMMAND_ID, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL, PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL, OPEN_LOADED_SCRIPTS_LABEL, SHOW_LOADED_SCRIPTS_ID, DEBUG_QUICK_ACCESS_PREFIX, DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL, STEP_INTO_TARGET_LABEL, STEP_INTO_TARGET_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { StatusBarColorProvider } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; @@ -57,6 +57,7 @@ import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassem import { DebugLifecycle } from 'vs/workbench/contrib/debug/common/debugLifecycle'; import { Icon } from 'vs/platform/action/common/action'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { DebugConsoleQuickAccess } from 'vs/workbench/contrib/debug/browser/debugConsoleQuickAccess'; const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); @@ -77,10 +78,19 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).regi // Register Quick Access Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ ctor: StartDebugQuickAccessProvider, - prefix: StartDebugQuickAccessProvider.PREFIX, + prefix: DEBUG_QUICK_ACCESS_PREFIX, contextKey: 'inLaunchConfigurationsPicker', placeholder: nls.localize('startDebugPlaceholder', "Type the name of a launch configuration to run."), - helpEntries: [{ description: nls.localize('startDebuggingHelp', "Start Debugging"), needsEditor: false }] + helpEntries: [{ description: nls.localize('startDebuggingHelp', "Start Debugging"), commandId: SELECT_AND_START_ID }] +}); + +// Register quick access for debug console +Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: DebugConsoleQuickAccess, + prefix: DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, + contextKey: 'inDebugConsolePicker', + placeholder: nls.localize('tasksQuickAccessPlaceholder', "Type the name of a debug console to open."), + helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Show All Debug Consoles"), commandId: SELECT_DEBUG_CONSOLE_ID }] }); @@ -104,6 +114,7 @@ registerDebugCommandPaletteItem(RESTART_SESSION_ID, RESTART_LABEL); registerDebugCommandPaletteItem(TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), CONTEXT_IN_DEBUG_MODE); registerDebugCommandPaletteItem(STEP_OVER_ID, STEP_OVER_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugCommandPaletteItem(STEP_INTO_ID, STEP_INTO_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); +registerDebugCommandPaletteItem(STEP_INTO_TARGET_ID, STEP_INTO_TARGET_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.and(CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); registerDebugCommandPaletteItem(STEP_OUT_ID, STEP_OUT_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugCommandPaletteItem(PAUSE_ID, PAUSE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('running')); registerDebugCommandPaletteItem(DISCONNECT_ID, DISCONNECT_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.or(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED)); @@ -120,6 +131,10 @@ registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlin registerDebugCommandPaletteItem(DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); registerDebugCommandPaletteItem(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); registerDebugCommandPaletteItem(SELECT_AND_START_ID, SELECT_AND_START_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); +registerDebugCommandPaletteItem(NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL); +registerDebugCommandPaletteItem(PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL); +registerDebugCommandPaletteItem(SHOW_LOADED_SCRIPTS_ID, OPEN_LOADED_SCRIPTS_LABEL, CONTEXT_IN_DEBUG_MODE); +registerDebugCommandPaletteItem(SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL); // Debug callstack context menu @@ -165,7 +180,7 @@ registerDebugViewMenuItem(MenuId.DebugWatchContext, EDIT_EXPRESSION_COMMAND_ID, registerDebugViewMenuItem(MenuId.DebugWatchContext, SET_EXPRESSION_COMMAND_ID, nls.localize('setValue', "Set Value"), 30, ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_SET_EXPRESSION_SUPPORTED), ContextKeyExpr.and(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable'), CONTEXT_SET_VARIABLE_SUPPORTED)), CONTEXT_VARIABLE_IS_READONLY.toNegated(), '3_modification'); registerDebugViewMenuItem(MenuId.DebugWatchContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 40, ContextKeyExpr.or(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable')), CONTEXT_IN_DEBUG_MODE, '3_modification'); registerDebugViewMenuItem(MenuId.DebugWatchContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Binary Data"), 50, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, '3_modification'); -registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_EXPRESSION_COMMAND_ID, nls.localize('removeWatchExpression', "Remove Expression"), 10, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, 'z_commands'); +registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_EXPRESSION_COMMAND_ID, nls.localize('removeWatchExpression', "Remove Expression"), 10, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, 'inline', icons.watchExpressionRemove); registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, 20, undefined, undefined, 'z_commands'); // Touch Bar diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 7a9ec3bbb3d..c4fbab00e3a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -303,10 +303,10 @@ export class AdapterManager extends Disposable implements IAdapterManager { return adapter && adapter.enabled ? adapter : undefined; } - isDebuggerInterestedInLanguage(language: string): boolean { + someDebuggerInterestedInLanguage(languageId: string): boolean { return !!this.debuggers .filter(d => d.enabled) - .find(a => language && a.languages && a.languages.indexOf(language) >= 0); + .find(a => a.interestedInLanguage(languageId)); } async guessDebugger(gettingConfigurations: boolean): Promise<Debugger | undefined> { @@ -322,7 +322,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { } const adapters = this.debuggers .filter(a => a.enabled) - .filter(a => language && a.languages && a.languages.indexOf(language) >= 0); + .filter(a => language && a.interestedInLanguage(language)); if (adapters.length === 1) { return adapters[0]; } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 3042792f8e8..49eb8197d7a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -8,7 +8,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService } from 'vs/platform/list/browser/listService'; -import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, State, getStateLabel, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, VIEWLET_ID, CONTEXT_DISASSEMBLY_VIEW_FOCUS } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, State, getStateLabel, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, VIEWLET_ID, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_REPL, CONTEXT_STEP_INTO_TARGETS_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable, Breakpoint, FunctionBreakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -25,12 +25,13 @@ import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/c import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { deepClone } from 'vs/base/common/objects'; import { isWeb, isWindows } from 'vs/base/common/platform'; import { saveAllBeforeDebugStart } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { showLoadedScriptMenu } from 'vs/workbench/contrib/debug/common/loadedScriptsPicker'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; @@ -41,6 +42,7 @@ export const RESTART_SESSION_ID = 'workbench.action.debug.restart'; export const TERMINATE_THREAD_ID = 'workbench.action.debug.terminateThread'; export const STEP_OVER_ID = 'workbench.action.debug.stepOver'; export const STEP_INTO_ID = 'workbench.action.debug.stepInto'; +export const STEP_INTO_TARGET_ID = 'workbench.action.debug.stepIntoTarget'; export const STEP_OUT_ID = 'workbench.action.debug.stepOut'; export const PAUSE_ID = 'workbench.action.debug.pause'; export const DISCONNECT_ID = 'workbench.action.debug.disconnect'; @@ -52,16 +54,21 @@ export const FOCUS_REPL_ID = 'workbench.debug.action.focusRepl'; export const JUMP_TO_CURSOR_ID = 'debug.jumpToCursor'; export const FOCUS_SESSION_ID = 'workbench.action.debug.focusProcess'; export const SELECT_AND_START_ID = 'workbench.action.debug.selectandstart'; +export const SELECT_DEBUG_CONSOLE_ID = 'workbench.action.debug.selectDebugConsole'; export const DEBUG_CONFIGURE_COMMAND_ID = 'workbench.action.debug.configure'; export const DEBUG_START_COMMAND_ID = 'workbench.action.debug.start'; export const DEBUG_RUN_COMMAND_ID = 'workbench.action.debug.run'; export const EDIT_EXPRESSION_COMMAND_ID = 'debug.renameWatchExpression'; export const SET_EXPRESSION_COMMAND_ID = 'debug.setWatchExpression'; export const REMOVE_EXPRESSION_COMMAND_ID = 'debug.removeWatchExpression'; +export const NEXT_DEBUG_CONSOLE_ID = 'workbench.action.debug.nextConsole'; +export const PREV_DEBUG_CONSOLE_ID = 'workbench.action.debug.prevConsole'; +export const SHOW_LOADED_SCRIPTS_ID = 'workbench.action.debug.showLoadedScripts'; export const RESTART_LABEL = nls.localize('restartDebug', "Restart"); export const STEP_OVER_LABEL = nls.localize('stepOverDebug', "Step Over"); export const STEP_INTO_LABEL = nls.localize('stepIntoDebug', "Step Into"); +export const STEP_INTO_TARGET_LABEL = nls.localize('stepIntoTargetDebug', "Step Into Target"); export const STEP_OUT_LABEL = nls.localize('stepOutDebug', "Step Out"); export const PAUSE_LABEL = nls.localize('pauseDebug', "Pause"); export const DISCONNECT_LABEL = nls.localize('disconnect', "Disconnect"); @@ -73,6 +80,14 @@ export const SELECT_AND_START_LABEL = nls.localize('selectAndStartDebugging', "S export const DEBUG_CONFIGURE_LABEL = nls.localize('openLaunchJson', "Open '{0}'", 'launch.json'); export const DEBUG_START_LABEL = nls.localize('startDebug', "Start Debugging"); export const DEBUG_RUN_LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); +export const NEXT_DEBUG_CONSOLE_LABEL = nls.localize('nextDebugConsole', "Focus Next Debug Console"); +export const PREV_DEBUG_CONSOLE_LABEL = nls.localize('prevDebugConsole', "Focus Previous Debug Console"); +export const OPEN_LOADED_SCRIPTS_LABEL = nls.localize('openLoadedScript', "Open Loaded Script..."); + +export const SELECT_DEBUG_CONSOLE_LABEL = nls.localize('selectDebugConsole', "Select Debug Console"); + +export const DEBUG_QUICK_ACCESS_PREFIX = 'debug '; +export const DEBUG_CONSOLE_QUICK_ACCESS_PREFIX = 'debug consoles '; interface CallStackContext { sessionId: string; @@ -136,6 +151,33 @@ function isSessionContext(obj: any): obj is CallStackContext { return obj && typeof obj.sessionId === 'string'; } +async function changeDebugConsoleFocus(accessor: ServicesAccessor, next: boolean) { + const debugService = accessor.get(IDebugService); + const viewsService = accessor.get(IViewsService); + const sessions = debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl()); + let currSession = debugService.getViewModel().focusedSession; + + let nextIndex = 0; + if (sessions.length > 0 && currSession) { + while (currSession && !currSession.hasSeparateRepl()) { + currSession = currSession.parentSession; + } + + if (currSession) { + const currIndex = sessions.indexOf(currSession); + if (next) { + nextIndex = (currIndex === (sessions.length - 1) ? 0 : (currIndex + 1)); + } else { + nextIndex = (currIndex === 0 ? (sessions.length - 1) : (currIndex - 1)); + } + } + } + await debugService.focusStackFrame(undefined, undefined, sessions[nextIndex], { explicit: true }); + + if (!viewsService.isViewVisible(REPL_VIEW_ID)) { + await viewsService.openView(REPL_VIEW_ID, true); + } +} // These commands are used in call stack context menu, call stack inline actions, command palette, debug toolbar, mac native touch bar // When the command is exectued in the context of a thread(context menu on a thread, inline call stack action) we pass the thread id @@ -230,6 +272,28 @@ MenuRegistry.appendMenuItem(MenuId.EditorContext, { }); KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: NEXT_DEBUG_CONSOLE_ID, + weight: KeybindingWeight.WorkbenchContrib + 1, + when: CONTEXT_IN_DEBUG_REPL, + primary: KeyMod.CtrlCmd | KeyCode.PageDown, + mac: { primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.BracketRight }, + handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { + changeDebugConsoleFocus(accessor, true); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: PREV_DEBUG_CONSOLE_ID, + weight: KeybindingWeight.WorkbenchContrib + 1, + when: CONTEXT_IN_DEBUG_REPL, + primary: KeyMod.CtrlCmd | KeyCode.PageUp, + mac: { primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.BracketLeft }, + handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { + changeDebugConsoleFocus(accessor, false); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ id: RESTART_SESSION_ID, weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.F5, @@ -274,10 +338,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +// Windows browsers use F11 for full screen, thus use alt+F11 as the default shortcut +const STEP_INTO_KEYBINDING = (isWeb && isWindows) ? (KeyMod.Alt | KeyCode.F11) : KeyCode.F11; + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: STEP_INTO_ID, weight: KeybindingWeight.WorkbenchContrib + 10, // Have a stronger weight to have priority over full screen when debugging - primary: (isWeb && isWindows) ? (KeyMod.Alt | KeyCode.F11) : KeyCode.F11, // Windows browsers use F11 for full screen, thus use alt+F11 as the default shortcut + primary: STEP_INTO_KEYBINDING, // Use a more flexible when clause to not allow full screen command to take over when F11 pressed a lot of times when: CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { @@ -315,6 +382,73 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: STEP_INTO_TARGET_ID, + primary: STEP_INTO_KEYBINDING | KeyMod.CtrlCmd, + when: ContextKeyExpr.and(CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')), + weight: KeybindingWeight.WorkbenchContrib, + handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { + const quickInputService = accessor.get(IQuickInputService); + const debugService = accessor.get(IDebugService); + const session = debugService.getViewModel().focusedSession; + const frame = debugService.getViewModel().focusedStackFrame; + if (!frame || !session) { + return; + } + + const editor = await accessor.get(IEditorService).openEditor({ + resource: frame.source.uri, + options: { revealIfOpened: true } + }); + + let codeEditor: ICodeEditor | undefined; + if (editor) { + const ctrl = editor?.getControl(); + if (isCodeEditor(ctrl)) { + codeEditor = ctrl; + } + } + + interface ITargetItem extends IQuickPickItem { + target: DebugProtocol.StepInTarget; + } + + const qp = quickInputService.createQuickPick<ITargetItem>(); + qp.busy = true; + qp.show(); + + qp.onDidChangeActive(([item]) => { + if (codeEditor && item && item.target.line !== undefined) { + codeEditor.revealLineInCenterIfOutsideViewport(item.target.line); + codeEditor.setSelection({ + startLineNumber: item.target.line, + startColumn: item.target.column || 1, + endLineNumber: item.target.endLine || item.target.line, + endColumn: item.target.endColumn || item.target.column || 1, + }); + } + }); + + qp.onDidAccept(() => { + if (qp.activeItems.length) { + session.stepIn(frame.thread.threadId, qp.activeItems[0].target.id); + } + }); + + qp.onDidHide(() => qp.dispose()); + + session.stepInTargets(frame.frameId).then(targets => { + qp.busy = false; + if (targets?.length) { + qp.items = targets?.map(target => ({ target, label: target.label })); + } else { + qp.placeholder = nls.localize('editor.debug.action.stepIntoTargets.none', "No step targets available"); + } + }); + } +}); + async function stopHandler(accessor: ServicesAccessor, _: string, context: CallStackContext | unknown, disconnect: boolean, suspend?: boolean): Promise<void> { const debugService = accessor.get(IDebugService); let session: IDebugSession | undefined; @@ -382,6 +516,15 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); CommandsRegistry.registerCommand({ + id: SHOW_LOADED_SCRIPTS_ID, + handler: async (accessor) => { + + await showLoadedScriptMenu(accessor); + + } +}); + +CommandsRegistry.registerCommand({ id: FOCUS_REPL_ID, handler: async (accessor) => { const viewsService = accessor.get(IViewsService); @@ -418,7 +561,15 @@ CommandsRegistry.registerCommand({ id: SELECT_AND_START_ID, handler: async (accessor: ServicesAccessor) => { const quickInputService = accessor.get(IQuickInputService); - quickInputService.quickAccess.show('debug '); + quickInputService.quickAccess.show(DEBUG_QUICK_ACCESS_PREFIX); + } +}); + +CommandsRegistry.registerCommand({ + id: SELECT_DEBUG_CONSOLE_ID, + handler: async (accessor: ServicesAccessor) => { + const quickInputService = accessor.get(IQuickInputService); + quickInputService.quickAccess.show(DEBUG_CONSOLE_QUICK_ACCESS_PREFIX); } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugConsoleQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugConsoleQuickAccess.ts new file mode 100644 index 00000000000..5479a5a393c --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugConsoleQuickAccess.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { FastAndSlowPicks, IPickerQuickAccessItem, PickerQuickAccessProvider, Picks } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IViewsService } from 'vs/workbench/common/views'; +import { DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, SELECT_AND_START_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { IDebugService, IDebugSession, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; + +export class DebugConsoleQuickAccess extends PickerQuickAccessProvider<IPickerQuickAccessItem> { + + constructor( + @IDebugService private readonly _debugService: IDebugService, + @IViewsService private readonly _viewsService: IViewsService, + @ICommandService private readonly _commandService: ICommandService, + ) { + super(DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, { canAcceptInBackground: true }); + } + + protected _getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Picks<IPickerQuickAccessItem> | Promise<Picks<IPickerQuickAccessItem>> | FastAndSlowPicks<IPickerQuickAccessItem> | null { + const debugConsolePicks: Array<IPickerQuickAccessItem | IQuickPickSeparator> = []; + + this._debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl()).forEach((session, index) => { + const pick = this._createPick(session, index, filter); + if (pick) { + debugConsolePicks.push(pick); + } + }); + + + if (debugConsolePicks.length > 0) { + debugConsolePicks.push({ type: 'separator' }); + } + + const createTerminalLabel = localize("workbench.action.debug.startDebug", "Start a New Debug Session"); + debugConsolePicks.push({ + label: `$(plus) ${createTerminalLabel}`, + ariaLabel: createTerminalLabel, + accept: () => this._commandService.executeCommand(SELECT_AND_START_ID) + }); + return debugConsolePicks; + } + + private _createPick(session: IDebugSession, sessionIndex: number, filter: string): IPickerQuickAccessItem | undefined { + const label = session.name; + + const highlights = matchesFuzzy(filter, label, true); + if (highlights) { + return { + label, + highlights: { label: highlights }, + accept: (keyMod, event) => { + this._debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); + if (!this._viewsService.isViewVisible(REPL_VIEW_ID)) { + this._viewsService.openView(REPL_VIEW_ID, true); + } + } + }; + } + return undefined; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 4264520e56a..9687e574a1a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -16,14 +16,16 @@ import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpo import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { PanelFocusContext } from 'vs/workbench/common/contextkeys'; import { IViewsService } from 'vs/workbench/common/views'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { Action } from 'vs/base/common/actions'; -import { getDomNodePagePosition } from 'vs/base/browser/dom'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { registerAction2, MenuId, Action2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; +import { getDomNodePagePosition } from 'vs/base/browser/dom'; +import { Position } from 'vs/editor/common/core/position'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { Action } from 'vs/base/common/actions'; class ToggleBreakpointAction extends EditorAction { constructor() { @@ -329,16 +331,18 @@ class ShowDebugHoverAction extends EditorAction { } } +const NO_TARGETS_MESSAGE = nls.localize('editor.debug.action.stepIntoTargets.notAvailable', "Step targets are not available here"); + class StepIntoTargetsAction extends EditorAction { public static readonly ID = 'editor.debug.action.stepIntoTargets'; - public static readonly LABEL = nls.localize({ key: 'stepIntoTargets', comment: ['Step Into Targets lets the user step into an exact function he or she is interested in.'] }, "Step Into Targets..."); + public static readonly LABEL = nls.localize({ key: 'stepIntoTargets', comment: ['Step Into Targets lets the user step into an exact function he or she is interested in.'] }, "Step Into Target"); constructor() { super({ id: StepIntoTargetsAction.ID, label: StepIntoTargetsAction.LABEL, - alias: 'Debug: Step Into Targets...', + alias: 'Debug: Step Into Target', precondition: ContextKeyExpr.and(CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), EditorContextKeys.editorTextFocus), contextMenuOpts: { group: 'debug', @@ -353,26 +357,63 @@ class StepIntoTargetsAction extends EditorAction { const uriIdentityService = accessor.get(IUriIdentityService); const session = debugService.getViewModel().focusedSession; const frame = debugService.getViewModel().focusedStackFrame; + const selection = editor.getSelection(); - if (session && frame && editor.hasModel() && uriIdentityService.extUri.isEqual(editor.getModel().uri, frame.source.uri)) { - const targets = await session.stepInTargets(frame.frameId); - if (!targets) { - return; + const targetPosition = selection?.getPosition() || (frame && { lineNumber: frame.range.startLineNumber, column: frame.range.startColumn }); + + if (!session || !frame || !editor.hasModel() || !uriIdentityService.extUri.isEqual(editor.getModel().uri, frame.source.uri)) { + if (targetPosition) { + MessageController.get(editor)?.showMessage(NO_TARGETS_MESSAGE, targetPosition); } + return; + } - editor.revealLineInCenterIfOutsideViewport(frame.range.startLineNumber); - const cursorCoords = editor.getScrolledVisiblePosition({ lineNumber: frame.range.startLineNumber, column: frame.range.startColumn }); - const editorCoords = getDomNodePagePosition(editor.getDomNode()); - const x = editorCoords.left + cursorCoords.left; - const y = editorCoords.top + cursorCoords.top + cursorCoords.height; - contextMenuService.showContextMenu({ - getAnchor: () => ({ x, y }), - getActions: () => { - return targets.map(t => new Action(`stepIntoTarget:${t.id}`, t.label, undefined, true, () => session.stepIn(frame.thread.threadId, t.id))); + const targets = await session.stepInTargets(frame.frameId); + if (!targets?.length) { + MessageController.get(editor)?.showMessage(NO_TARGETS_MESSAGE, targetPosition!); + return; + } + + // If there is a selection, try to find the best target with a position to step into. + if (selection) { + const positionalTargets: { start: Position; end?: Position; target: DebugProtocol.StepInTarget }[] = []; + for (const target of targets) { + if (target.line) { + positionalTargets.push({ + start: new Position(target.line, target.column || 1), + end: target.endLine ? new Position(target.endLine, target.endColumn || 1) : undefined, + target + }); } - }); + } + + positionalTargets.sort((a, b) => b.start.lineNumber - a.start.lineNumber || b.start.column - a.start.column); + + const needle = selection.getPosition(); + + // Try to find a target with a start and end that is around the cursor + // position. Or, if none, whatever is before the cursor. + const best = positionalTargets.find(t => t.end && needle.isBefore(t.end) && t.start.isBeforeOrEqual(needle)) || positionalTargets.find(t => t.end === undefined && t.start.isBeforeOrEqual(needle)); + if (best) { + session.stepIn(frame.thread.threadId, best.target.id); + return; + } } + + // Otherwise, show a context menu and have the user pick a target + editor.revealLineInCenterIfOutsideViewport(frame.range.startLineNumber); + const cursorCoords = editor.getScrolledVisiblePosition(targetPosition!); + const editorCoords = getDomNodePagePosition(editor.getDomNode()); + const x = editorCoords.left + cursorCoords.left; + const y = editorCoords.top + cursorCoords.top + cursorCoords.height; + + contextMenuService.showContextMenu({ + getAnchor: () => ({ x, y }), + getActions: () => { + return targets.map(t => new Action(`stepIntoTarget:${t.id}`, t.label, undefined, true, () => session.stepIn(frame.thread.threadId, t.id))); + } + }); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 98bcc2d1d6f..48a002966c6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -12,7 +12,8 @@ import { setProperty } from 'vs/base/common/jsonEdit'; import { Constants } from 'vs/base/common/uint'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { InlineValueContext, StandardTokenType } from 'vs/editor/common/languages'; +import { InlineValueContext } from 'vs/editor/common/languages'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { distinct, flatten } from 'vs/base/common/arrays'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index ac91e22355a..4da189b0aff 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -75,6 +75,7 @@ export const debugCollapseAll = registerIcon('debug-collapse-all', Codicon.colla export const callstackViewSession = registerIcon('callstack-view-session', Codicon.bug, localize('callstackViewSession', 'Icon for the session icon in the call stack view.')); export const debugConsoleClearAll = registerIcon('debug-console-clear-all', Codicon.clearAll, localize('debugConsoleClearAll', 'Icon for the clear all action in the debug console.')); export const watchExpressionsRemoveAll = registerIcon('watch-expressions-remove-all', Codicon.closeAll, localize('watchExpressionsRemoveAll', 'Icon for the Remove All action in the watch view.')); +export const watchExpressionRemove = registerIcon('watch-expression-remove', Codicon.removeClose, localize('watchExpressionRemove', 'Icon for the Remove action in the watch view.')); export const watchExpressionsAdd = registerIcon('watch-expressions-add', Codicon.add, localize('watchExpressionsAdd', 'Icon for the add action in the watch view.')); export const watchExpressionsAddFuncBreakpoint = registerIcon('watch-expressions-add-function-breakpoint', Codicon.add, localize('watchExpressionsAddFuncBreakpoint', 'Icon for the add function breakpoint action in the watch view.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts index 06e1449c8f2..df5f79031ea 100644 --- a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts +++ b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -12,21 +12,19 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { ICommandService } from 'vs/platform/commands/common/commands'; import { matchesFuzzy } from 'vs/base/common/filters'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { ADD_CONFIGURATION_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { ADD_CONFIGURATION_ID, DEBUG_QUICK_ACCESS_PREFIX } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { debugConfigure, debugRemoveConfig } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider<IPickerQuickAccessItem> { - static PREFIX = 'debug '; - constructor( @IDebugService private readonly debugService: IDebugService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ICommandService private readonly commandService: ICommandService, @INotificationService private readonly notificationService: INotificationService, ) { - super(StartDebugQuickAccessProvider.PREFIX, { + super(DEBUG_QUICK_ACCESS_PREFIX, { noResultsPick: { label: localize('noDebugResults', "No matching launch configurations") } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 50c7f78b6b2..b308716a8bb 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -690,8 +690,11 @@ export class DebugService implements IDebugService { const dataBreakpoints = this.model.getDataBreakpoints().filter(dbp => !dbp.canPersist); dataBreakpoints.forEach(dbp => this.model.removeDataBreakpoints(dbp.getId())); - if (this.viewsService.isViewVisible(REPL_VIEW_ID) && this.configurationService.getValue<IDebugConfiguration>('debug').console.closeOnEnd) { - this.viewsService.closeView(REPL_VIEW_ID); + if (this.configurationService.getValue<IDebugConfiguration>('debug').console.closeOnEnd) { + const debugConsoleContainer = this.viewDescriptorService.getViewContainerByViewId(REPL_VIEW_ID); + if (debugConsoleContainer && this.viewsService.isViewContainerVisible(debugConsoleContainer.id)) { + this.viewsService.closeViewContainer(debugConsoleContainer.id); + } } } })); @@ -867,8 +870,8 @@ export class DebugService implements IDebugService { const lineNumber = stackFrame.range.startLineNumber; if (lineNumber >= 1 && lineNumber <= model.getLineCount()) { const lineContent = control.getModel().getLineContent(lineNumber); - aria.alert(nls.localize({ key: 'debuggingPaused', comment: ['First placeholder is the stack frame name, second is the line number, third placeholder is the reason why debugging is stopped, for example "breakpoint" and the last one is the file line content.'] }, - "{0}:{1}, debugging paused {2}, {3}", stackFrame.source ? stackFrame.source.name : '', stackFrame.range.startLineNumber, thread && thread.stoppedDetails ? `, reason ${thread.stoppedDetails.reason}` : '', lineContent)); + aria.alert(nls.localize({ key: 'debuggingPaused', comment: ['First placeholder is the file line content, second placeholder is the reason why debugging is stopped, for example "breakpoint", third is the stack frame name, and last is the line number.'] }, + "{0}, debugging paused {1}, {2}:{3}", lineContent, thread && thread.stoppedDetails ? `, reason ${thread.stoppedDetails.reason}` : '', stackFrame.source ? stackFrame.source.name : '', stackFrame.range.startLineNumber)); } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 3ff60d94d24..4a53a4e0c93 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -925,9 +925,7 @@ export class DebugSession implements IDebugSession { } catch (e) { // Disconnect the debug session on configuration done error #10596 this.notificationService.error(e); - if (this.raw) { - this.raw.disconnect({}); - } + this.raw?.disconnect({}); } } @@ -1030,9 +1028,7 @@ export class DebugSession implements IDebugSession { this.stoppedDetails = this.stoppedDetails.filter(sd => sd.threadId !== threadId); const tokens = this.cancellationMap.get(threadId); this.cancellationMap.delete(threadId); - if (tokens) { - tokens.forEach(t => t.cancel()); - } + tokens?.forEach(t => t.cancel()); } else { this.stoppedDetails = []; this.cancelAllRequests(); diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index 898b0329b5e..c4a8ef6be74 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -49,9 +49,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { } })); this.toDispose.push(this.debugService.getConfigurationManager().onDidSelectConfiguration(e => { - if (this.entryAccessor) { - this.entryAccessor.update(this.entry); - } + this.entryAccessor?.update(this.entry); })); } diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 4667fc38a3e..d9ee132c153 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -6,11 +6,11 @@ import * as nls from 'vs/nls'; import severity from 'vs/base/common/severity'; import { Event } from 'vs/base/common/event'; -import Constants from 'vs/workbench/contrib/markers/browser/constants'; +import { Markers } from 'vs/workbench/contrib/markers/common/markers'; import { ITaskService, ITaskSummary } from 'vs/workbench/contrib/tasks/common/taskService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; -import { TaskEvent, TaskEventKind, TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskEvent, TaskEventKind, ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; @@ -22,7 +22,7 @@ import { Action } from 'vs/base/common/actions'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ICommandService } from 'vs/platform/commands/common/commands'; -function once(match: (e: TaskEvent) => boolean, event: Event<TaskEvent>): Event<TaskEvent> { +function once(match: (e: ITaskEvent) => boolean, event: Event<ITaskEvent>): Event<ITaskEvent> { return (listener, thisArgs = null, disposables?) => { const result = event(e => { if (match(e)) { @@ -59,7 +59,7 @@ export class DebugTaskRunner { this.canceled = true; } - async runTaskAndCheckErrors(root: IWorkspaceFolder | IWorkspace | undefined, taskId: string | TaskIdentifier | undefined): Promise<TaskRunResult> { + async runTaskAndCheckErrors(root: IWorkspaceFolder | IWorkspace | undefined, taskId: string | ITaskIdentifier | undefined): Promise<TaskRunResult> { try { this.canceled = false; const taskSummary = await this.runTask(root, taskId); @@ -76,7 +76,7 @@ export class DebugTaskRunner { return TaskRunResult.Success; } if (onTaskErrors === 'showErrors') { - await this.viewsService.openView(Constants.MARKERS_VIEW_ID, true); + await this.viewsService.openView(Markers.MARKERS_VIEW_ID, true); return Promise.resolve(TaskRunResult.Failure); } if (onTaskErrors === 'abort') { @@ -113,7 +113,7 @@ export class DebugTaskRunner { return TaskRunResult.Success; } - await this.viewsService.openView(Constants.MARKERS_VIEW_ID, true); + await this.viewsService.openView(Markers.MARKERS_VIEW_ID, true); return Promise.resolve(TaskRunResult.Failure); } catch (err) { const taskConfigureAction = this.taskService.configureAction(); @@ -149,7 +149,7 @@ export class DebugTaskRunner { } } - async runTask(root: IWorkspace | IWorkspaceFolder | undefined, taskId: string | TaskIdentifier | undefined): Promise<ITaskSummary | null> { + async runTask(root: IWorkspace | IWorkspaceFolder | undefined, taskId: string | ITaskIdentifier | undefined): Promise<ITaskSummary | null> { if (!taskId) { return Promise.resolve(null); } diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 202926d5c34..1f7614bfee7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -181,7 +181,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { const left = dom.getComputedStyle(this.$el).left; if (left) { const position = parseFloat(left) / window.innerWidth; - this.storageService.store(DEBUG_TOOLBAR_POSITION_KEY, position, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(DEBUG_TOOLBAR_POSITION_KEY, position, StorageScope.PROFILE, StorageTarget.MACHINE); } } @@ -218,7 +218,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } const widgetWidth = this.$el.clientWidth; if (x === undefined) { - const positionPercentage = this.storageService.get(DEBUG_TOOLBAR_POSITION_KEY, StorageScope.GLOBAL); + const positionPercentage = this.storageService.get(DEBUG_TOOLBAR_POSITION_KEY, StorageScope.PROFILE); x = positionPercentage !== undefined ? parseFloat(positionPercentage) * window.innerWidth : (0.5 * window.innerWidth - 0.5 * widgetWidth); } @@ -226,13 +226,13 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { this.$el.style.left = `${x}px`; if (y === undefined) { - y = this.storageService.getNumber(DEBUG_TOOLBAR_Y_KEY, StorageScope.GLOBAL, 0); + y = this.storageService.getNumber(DEBUG_TOOLBAR_Y_KEY, StorageScope.PROFILE, 0); } const titleAreaHeight = 35; if ((y < titleAreaHeight / 2) || (y > titleAreaHeight + titleAreaHeight / 2)) { const moveToTop = y < titleAreaHeight; this.setYCoordinate(moveToTop ? 0 : titleAreaHeight); - this.storageService.store(DEBUG_TOOLBAR_Y_KEY, moveToTop ? 0 : 2 * titleAreaHeight, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(DEBUG_TOOLBAR_Y_KEY, moveToTop ? 0 : 2 * titleAreaHeight, StorageScope.PROFILE, StorageTarget.MACHINE); } } diff --git a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index cc58a507c36..2bfc78a80ae 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -284,9 +284,7 @@ export class DisassemblyView extends EditorPane { } layout(dimension: Dimension): void { - if (this._disassembledInstructions) { - this._disassembledInstructions.layout(dimension.height); - } + this._disassembledInstructions?.layout(dimension.height); } /** @@ -800,10 +798,10 @@ export class DisassemblyViewContribution implements IWorkbenchContribution { const language = activeTextEditorControl.getModel()?.getLanguageId(); // TODO: instead of using idDebuggerInterestedInLanguage, have a specific ext point for languages // support disassembly - this._languageSupportsDisassemleRequest?.set(!!language && debugService.getAdapterManager().isDebuggerInterestedInLanguage(language)); + this._languageSupportsDisassemleRequest?.set(!!language && debugService.getAdapterManager().someDebuggerInterestedInLanguage(language)); this._onDidChangeModelLanguage = activeTextEditorControl.onDidChangeModelLanguage(e => { - this._languageSupportsDisassemleRequest?.set(debugService.getAdapterManager().isDebuggerInterestedInLanguage(e.newLanguage)); + this._languageSupportsDisassemleRequest?.set(debugService.getAdapterManager().someDebuggerInterestedInLanguage(e.newLanguage)); }); } else { this._languageSupportsDisassemleRequest?.set(false); diff --git a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts index 672f00f8465..a3fda1a7045 100644 --- a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts +++ b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts @@ -78,9 +78,9 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i const workspaceId = toWorkspaceIdentifier(contextService.getWorkspace()); if (isSingleFolderWorkspaceIdentifier(workspaceId) || isWorkspaceIdentifier(workspaceId)) { const serializedWorkspace = isSingleFolderWorkspaceIdentifier(workspaceId) ? { folderUri: workspaceId.uri.toJSON() } : { workspaceUri: workspaceId.configPath.toJSON() }; - storageService.store(BrowserExtensionHostDebugService.LAST_EXTENSION_DEVELOPMENT_WORKSPACE_KEY, JSON.stringify(serializedWorkspace), StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(BrowserExtensionHostDebugService.LAST_EXTENSION_DEVELOPMENT_WORKSPACE_KEY, JSON.stringify(serializedWorkspace), StorageScope.PROFILE, StorageTarget.USER); } else { - storageService.remove(BrowserExtensionHostDebugService.LAST_EXTENSION_DEVELOPMENT_WORKSPACE_KEY, StorageScope.GLOBAL); + storageService.remove(BrowserExtensionHostDebugService.LAST_EXTENSION_DEVELOPMENT_WORKSPACE_KEY, StorageScope.PROFILE); } } } @@ -125,7 +125,7 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i const extensionTestsPath = this.findArgument('extensionTestsPath', args); if (!debugWorkspace && !extensionTestsPath) { - const lastExtensionDevelopmentWorkspace = this.storageService.get(BrowserExtensionHostDebugService.LAST_EXTENSION_DEVELOPMENT_WORKSPACE_KEY, StorageScope.GLOBAL); + const lastExtensionDevelopmentWorkspace = this.storageService.get(BrowserExtensionHostDebugService.LAST_EXTENSION_DEVELOPMENT_WORKSPACE_KEY, StorageScope.PROFILE); if (lastExtensionDevelopmentWorkspace) { try { const serializedWorkspace: { workspaceUri?: UriComponents; folderUri?: UriComponents } = JSON.parse(lastExtensionDevelopmentWorkspace); diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index 253bde7bdf8..d6cd075ffa2 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -27,6 +27,7 @@ const WIN_PATH = new RegExp(`(${WIN_ABSOLUTE_PATH.source}|${WIN_RELATIVE_PATH.so const POSIX_PATH = /((?:\~|\.)?(?:\/[\w\.-]*)+)/; const LINE_COLUMN = /(?:\:([\d]+))?(?:\:([\d]+))?/; const PATH_LINK_REGEX = new RegExp(`${platform.isWindows ? WIN_PATH.source : POSIX_PATH.source}${LINE_COLUMN.source}`, 'g'); +const LINE_COLUMN_REGEX = /:([\d]+)(?::([\d]+))?$/; const MAX_LENGTH = 2000; @@ -104,7 +105,17 @@ export class LinkDetector { private createWebLink(url: string): Node { const link = this.createLink(url); - const uri = URI.parse(url); + let uri = URI.parse(url); + // if the URI ends with something like `foo.js:12:3`, parse + // that into a fragment to reveal that location (#150702) + const lineCol = LINE_COLUMN_REGEX.exec(uri.path); + if (lineCol) { + uri = uri.with({ + path: uri.path.slice(0, lineCol.index), + fragment: `L${lineCol[0].slice(1)}` + }); + } + this.decorateLink(link, uri, async () => { if (uri.scheme === Schemas.file) { @@ -119,7 +130,13 @@ export class LinkDetector { return; } - await this.editorService.openEditor({ resource: fileUri, options: { pinned: true } }); + await this.editorService.openEditor({ + resource: fileUri, + options: { + pinned: true, + selection: lineCol ? { startLineNumber: +lineCol[1], startColumn: +lineCol[2] } : undefined, + }, + }); return; } diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 57a946206a8..0fe0b09bd92 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -317,3 +317,12 @@ overflow: hidden; text-overflow: ellipsis } + +.debug-pane .pane-header .breakpoint-warning { + margin-left: 3px; +} + +.debug-pane .pane-header .breakpoint-warning .monaco-icon-label .codicon { + display: flex; + align-items: center; +} diff --git a/src/vs/workbench/contrib/debug/browser/media/exceptionWidget.css b/src/vs/workbench/contrib/debug/browser/media/exceptionWidget.css index f9d7846d944..30efbfe88be 100644 --- a/src/vs/workbench/contrib/debug/browser/media/exceptionWidget.css +++ b/src/vs/workbench/contrib/debug/browser/media/exceptionWidget.css @@ -20,6 +20,8 @@ .monaco-editor .zone-widget .zone-widget-container.exception-widget .title .label { font-weight: bold; + display: flex; + align-items: center; } .monaco-editor .zone-widget .zone-widget-container.exception-widget .title .actions { diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 38c3431d3b3..6803c044720 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -39,8 +39,12 @@ word-break: break-all; } -.monaco-workbench.mac .repl .repl-tree .monaco-tl-twistie.collapsible + .monaco-tl-contents, -.monaco-workbench.mac .repl .repl-tree .monaco-tl-twistie { +.monaco-workbench .repl .repl-tree.word-wrap .monaco-tl-contents .expression.nested-variable { + white-space: pre; /* Preserve whitespace but don't wrap */ +} + +.monaco-workbench .repl .repl-tree .monaco-tl-twistie.collapsible + .monaco-tl-contents, +.monaco-workbench .repl .repl-tree .monaco-tl-twistie { cursor: pointer; } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index a38c878e3bc..7fcf2fa1c08 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -718,7 +718,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { }); } - this.replInput.setDecorations('repl-decoration', DECORATION_KEY, decorations); + this.replInput.setDecorationsByType('repl-decoration', DECORATION_KEY, decorations); } override saveState(): void { diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 6f5a97f4fad..b714e1e1181 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -239,6 +239,7 @@ export class ReplVariablesRenderer extends AbstractExpressionsRenderer { protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { renderVariable(expression as Variable, data, true, highlights, this.linkDetector); + data.expression.classList.toggle('nested-variable', isNestedVariable(expression)); } protected getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 677b8c86440..d4e995fb9f1 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -96,10 +96,10 @@ export class VariablesView extends ViewPane { const viewState = this.savedViewState.get(stackFrame.getId()); await this.tree.setInput(stackFrame, viewState); - // Automatically expand the first scope if it is not expensive and if all scopes are collapsed + // Automatically expand the first non-expensive scope const scopes = await stackFrame.getScopes(); const toExpand = scopes.find(s => !s.expensive); - if (toExpand && (scopes.every(s => this.tree.isCollapsed(s)) || !this.autoExpandedScopes.has(toExpand.getId()))) { + if (toExpand) { this.autoExpandedScopes.add(toExpand.getId()); await this.tree.expand(toExpand); } @@ -541,6 +541,7 @@ CommandsRegistry.registerCommand({ if (ext || await tryInstallHexEditor(notifications, progressService, extensionService, commandService)) { /* __GDPR__ "debug/didViewMemory" : { + "owner": "connor4312", "debugType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 231269cfc63..8dedf05cd31 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -7,13 +7,14 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDebugService, IExpression, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, WATCH_VIEW_ID, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_VARIABLE_IS_READONLY } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAction } from 'vs/base/common/actions'; import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -268,13 +269,32 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { static readonly ID = 'watchexpression'; + constructor( + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IDebugService debugService: IDebugService, + @IContextViewService contextViewService: IContextViewService, + @IThemeService themeService: IThemeService, + ) { + super(debugService, contextViewService, themeService); + } + get templateId() { return WatchExpressionsRenderer.ID; } protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { const text = typeof expression.value === 'string' ? `${expression.name}:` : expression.name; - data.label.set(text, highlights, expression.type ? expression.type : expression.value); + let title: string; + if (expression.type) { + title = expression.type === expression.value ? + expression.type : + `${expression.type}: ${expression.value}`; + } else { + title = expression.value; + } + + data.label.set(text, highlights, title); renderExpressionValue(expression, data.value, { showChanged: true, maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, @@ -316,6 +336,28 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { } }; } + + protected override renderActionBar(actionBar: ActionBar, expression: IExpression, data: IExpressionTemplateData) { + const contextKeyService = getContextForWatchExpressionMenu(this.contextKeyService); + const menu = this.menuService.createMenu(MenuId.DebugWatchContext, contextKeyService); + + const primary: IAction[] = []; + const context = expression; + data.elementDisposable.push(createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary, secondary: [] }, 'inline')); + + actionBar.clear(); + actionBar.context = context; + actionBar.push(primary, { icon: true, label: false }); + } +} + +/** + * Gets a context key overlay that has context for the given expression. + */ +function getContextForWatchExpressionMenu(parentContext: IContextKeyService) { + return parentContext.createOverlay([ + [CONTEXT_WATCH_ITEM_TYPE.key, 'expression'] + ]); } class WatchExpressionsAccessibilityProvider implements IListAccessibilityProvider<IExpression> { diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index c936f453d32..28ca270995d 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -65,7 +65,7 @@ export class WelcomeView extends ViewPane { if (isCodeEditor(editorControl)) { const model = editorControl.getModel(); const language = model ? model.getLanguageId() : undefined; - if (language && this.debugService.getAdapterManager().isDebuggerInterestedInLanguage(language)) { + if (language && this.debugService.getAdapterManager().someDebuggerInterestedInLanguage(language)) { this.debugStartLanguageContext.set(language); this.debuggerInterestedContext.set(true); storageSevice.store(debugStartLanguageKey, language, StorageScope.WORKSPACE, StorageTarget.MACHINE); diff --git a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts index 348aa5815b7..299a6ad3eb6 100644 --- a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts +++ b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts @@ -155,14 +155,10 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { switch (message.type) { case 'event': - if (this.eventCallback) { - this.eventCallback(<DebugProtocol.Event>message); - } + this.eventCallback?.(<DebugProtocol.Event>message); break; case 'request': - if (this.requestCallback) { - this.requestCallback(<DebugProtocol.Request>message); - } + this.requestCallback?.(<DebugProtocol.Request>message); break; case 'response': { const response = <DebugProtocol.Response>message; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 9457ae8c1b4..baac9206484 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,7 +24,7 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; -import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -159,6 +159,13 @@ export interface IDebugger { getCustomTelemetryEndpoint(): ITelemetryEndpoint | undefined; } +export interface IDebuggerMetadata { + label: string; + type: string; + uiMessages?: { [key in DebuggerUiMessage]: string }; + interestedInLanguage(languageId: string): boolean; +} + export const enum State { Inactive, Initializing, @@ -366,7 +373,7 @@ export interface IDebugSession extends ITreeElement { restartFrame(frameId: number, threadId: number): Promise<void>; next(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise<void>; stepIn(threadId: number, targetId?: number, granularity?: DebugProtocol.SteppingGranularity): Promise<void>; - stepInTargets(frameId: number): Promise<{ id: number; label: string }[] | undefined>; + stepInTargets(frameId: number): Promise<DebugProtocol.StepInTarget[] | undefined>; stepOut(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise<void>; stepBack(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise<void>; continue(threadId: number): Promise<void>; @@ -650,10 +657,10 @@ export interface IGlobalConfig { export interface IEnvConfig { internalConsoleOptions?: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; - preRestartTask?: string | TaskIdentifier; - postRestartTask?: string | TaskIdentifier; - preLaunchTask?: string | TaskIdentifier; - postDebugTask?: string | TaskIdentifier; + preRestartTask?: string | ITaskIdentifier; + postRestartTask?: string | ITaskIdentifier; + preLaunchTask?: string | ITaskIdentifier; + postDebugTask?: string | ITaskIdentifier; debugServer?: number; noDebug?: boolean; } @@ -687,7 +694,7 @@ export interface IConfig extends IEnvConfig { export interface ICompound { name: string; stopAll?: boolean; - preLaunchTask?: string | TaskIdentifier; + preLaunchTask?: string | ITaskIdentifier; configurations: (string | { name: string; folder: string })[]; presentation?: IConfigPresentation; } @@ -772,6 +779,8 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut configurationSnippets?: IJSONSchemaSnippet[]; variables?: { [key: string]: string }; when?: string; + deprecated?: string; + uiMessages?: { [key in DebuggerUiMessage]: string }; } export interface IBreakpointContribution { @@ -847,6 +856,10 @@ export interface IConfigurationManager { resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any, token: CancellationToken): Promise<any>; } +export enum DebuggerUiMessage { + UnverifiedBreakpoints = 'unverifiedBreakpoints' +} + export interface IAdapterManager { onDidRegisterDebugger: Event<void>; @@ -854,7 +867,8 @@ export interface IAdapterManager { hasEnabledDebuggers(): boolean; getDebugAdapterDescriptor(session: IDebugSession): Promise<IAdapterDescriptor | undefined>; getDebuggerLabel(type: string): string | undefined; - isDebuggerInterestedInLanguage(language: string): boolean; + someDebuggerInterestedInLanguage(language: string): boolean; + getDebugger(type: string): IDebuggerMetadata | undefined; activateDebuggers(activationEvent: string, debugType?: string): Promise<void>; registerDebugAdapterFactory(debugTypes: string[], debugAdapterFactory: IDebugAdapterFactory): IDisposable; diff --git a/src/vs/workbench/contrib/debug/common/debugContentProvider.ts b/src/vs/workbench/contrib/debug/common/debugContentProvider.ts index 772193c7f16..5cdb5c988cb 100644 --- a/src/vs/workbench/contrib/debug/common/debugContentProvider.ts +++ b/src/vs/workbench/contrib/debug/common/debugContentProvider.ts @@ -62,9 +62,7 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC * If there is no model for the given resource, this method does nothing. */ static refreshDebugContent(resource: uri): void { - if (DebugContentProvider.INSTANCE) { - DebugContentProvider.INSTANCE.createOrUpdateContentModel(resource, false); - } + DebugContentProvider.INSTANCE?.createOrUpdateContentModel(resource, false); } /** diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index c05fc04c956..4b20e069cd2 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -7,6 +7,7 @@ import { distinct, lastIndex } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { stringHash } from 'vs/base/common/hash'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { mixin } from 'vs/base/common/objects'; @@ -351,7 +352,7 @@ export class Scope extends ExpressionContainer implements IScope { constructor( stackFrame: IStackFrame, - index: number, + id: number, public readonly name: string, reference: number, public expensive: boolean, @@ -359,7 +360,7 @@ export class Scope extends ExpressionContainer implements IScope { indexedVariables?: number, public readonly range?: IRange ) { - super(stackFrame.thread.session, stackFrame.thread.threadId, reference, `scope:${name}:${index}`, namedVariables, indexedVariables); + super(stackFrame.thread.session, stackFrame.thread.threadId, reference, `scope:${name}:${id}`, namedVariables, indexedVariables); } override toString(): string { @@ -417,12 +418,17 @@ export class StackFrame implements IStackFrame { return []; } - const scopeNameIndexes = new Map<string, number>(); + const usedIds = new Set<number>(); return response.body.scopes.map(rs => { - const previousIndex = scopeNameIndexes.get(rs.name); - const index = typeof previousIndex === 'number' ? previousIndex + 1 : 0; - scopeNameIndexes.set(rs.name, index); - return new Scope(this, index, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables, + // form the id based on the name and location so that it's the + // same across multiple pauses to retain expansion state + let id = 0; + do { + id = stringHash(`${rs.name}:${rs.line}:${rs.column}`, id); + } while (usedIds.has(id)); + + usedIds.add(id); + return new Scope(this, id, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables, rs.line && rs.column && rs.endLine && rs.endColumn ? new Range(rs.line, rs.column, rs.endLine, rs.endColumn) : undefined); }); diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 656792a79d0..b5a858f7e52 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -49,7 +49,7 @@ declare module DebugProtocol { /** The command requested. */ command: string; /** Contains the raw error in short form if 'success' is false. - This raw error might be interpreted by the frontend and is not shown in the UI. + This raw error might be interpreted by the client and is not shown in the UI. Some predefined values exist. Values: 'cancelled': request was cancelled. @@ -69,16 +69,15 @@ declare module DebugProtocol { } /** Cancel request; value of command field is 'cancel'. - The 'cancel' request is used by the frontend in two situations: + The 'cancel' request is used by the client in two situations: - to indicate that it is no longer interested in the result produced by a specific request issued earlier - to cancel a progress sequence. Clients should only call this request if the capability 'supportsCancelRequest' is true. This request has a hint characteristic: a debug adapter can only be expected to make a 'best effort' in honouring this request but there are no guarantees. - The 'cancel' request may return an error if it could not cancel an operation but a frontend should refrain from presenting this error to end users. - A frontend client should only call this request if the capability 'supportsCancelRequest' is true. - The request that got canceled still needs to send a response back. This can either be a normal result ('success' attribute true) - or an error response ('success' attribute false and the 'message' set to 'cancelled'). - Returning partial results from a cancelled request is possible but please note that a frontend client has no generic way for detecting that a response is partial or not. - The progress that got cancelled still needs to send a 'progressEnd' event back. + The 'cancel' request may return an error if it could not cancel an operation but a client should refrain from presenting this error to end users. + A client should only call this request if the capability 'supportsCancelRequest' is true. + The request that got cancelled still needs to send a response back. This can either be a normal result ('success' attribute true) or an error response ('success' attribute false and the 'message' set to 'cancelled'). + Returning partial results from a cancelled request is possible but please note that a client has no generic way for detecting that a response is partial or not. + The progress that got cancelled still needs to send a 'progressEnd' event back. A client should not assume that progress just got cancelled after sending the 'cancel' request. */ interface CancelRequest extends Request { @@ -107,11 +106,11 @@ declare module DebugProtocol { A debug adapter is expected to send this event when it is ready to accept configuration requests (but not before the 'initialize' request has finished). The sequence of events/requests is as follows: - adapters sends 'initialized' event (after the 'initialize' request has returned) - - frontend sends zero or more 'setBreakpoints' requests - - frontend sends one 'setFunctionBreakpoints' request (if capability 'supportsFunctionBreakpoints' is true) - - frontend sends a 'setExceptionBreakpoints' request if one or more 'exceptionBreakpointFilters' have been defined (or if 'supportsConfigurationDoneRequest' is not defined or false) - - frontend sends other future configuration requests - - frontend sends one 'configurationDone' request to indicate the end of the configuration. + - client sends zero or more 'setBreakpoints' requests + - client sends one 'setFunctionBreakpoints' request (if capability 'supportsFunctionBreakpoints' is true) + - client sends a 'setExceptionBreakpoints' request if one or more 'exceptionBreakpointFilters' have been defined (or if 'supportsConfigurationDoneRequest' is not true) + - client sends other future configuration requests + - client sends one 'configurationDone' request to indicate the end of the configuration. */ interface InitializedEvent extends Event { // event: 'initialized'; @@ -133,7 +132,7 @@ declare module DebugProtocol { description?: string; /** The thread which was stopped. */ threadId?: number; - /** A value of true hints to the frontend that this event should not change the focus. */ + /** A value of true hints to the client that this event should not change the focus. */ preserveFocusHint?: boolean; /** Additional information. E.g. if reason is 'exception', text contains the exception name. This string is shown in the UI. */ text?: string; @@ -211,10 +210,10 @@ declare module DebugProtocol { interface OutputEvent extends Event { // event: 'output'; body: { - /** The output category. If not specified or if the category is not understand by the client, 'console' is assumed. + /** The output category. If not specified or if the category is not understood by the client, 'console' is assumed. Values: 'console': Show the output in the client's default message UI, e.g. a 'debug console'. This category should only be used for informational output from the debugger (as opposed to the debuggee). - 'important': A hint for the client to show the ouput in the client's UI for important and highly visible information, e.g. as a popup notification. This category should only be used for important messages from the debugger (as opposed to the debuggee). Since this category value is a hint, clients might ignore the hint and assume the 'console' category. + 'important': A hint for the client to show the output in the client's UI for important and highly visible information, e.g. as a popup notification. This category should only be used for important messages from the debugger (as opposed to the debuggee). Since this category value is a hint, clients might ignore the hint and assume the 'console' category. 'stdout': Show the output as normal program output from the debuggee. 'stderr': Show the output as error program output from the debuggee. 'telemetry': Send the output to telemetry instead of showing it to the user. @@ -228,8 +227,8 @@ declare module DebugProtocol { The 'output' attribute becomes the name of the group and is not indented. 'startCollapsed': Start a new group in collapsed mode. Subsequent output events are members of the group and should be shown indented (as soon as the group is expanded). The 'output' attribute becomes the name of the group and is not indented. - 'end': End the current group and decreases the indentation of subsequent output events. - A non empty 'output' attribute is shown as the unindented end of the group. + 'end': End the current group and decrease the indentation of subsequent output events. + A nonempty 'output' attribute is shown as the unindented end of the group. */ group?: 'start' | 'startCollapsed' | 'end'; /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing 'variablesReference' to the 'variables' request. The value should be less than or equal to 2147483647 (2^31-1). */ @@ -255,7 +254,7 @@ declare module DebugProtocol { Values: 'changed', 'new', 'removed', etc. */ reason: 'changed' | 'new' | 'removed' | string; - /** The 'id' attribute is used to find the target breakpoint and the other attributes are used as the new values. */ + /** The 'id' attribute is used to find the target breakpoint, the other attributes are used as the new values. */ breakpoint: Breakpoint; }; } @@ -311,8 +310,8 @@ declare module DebugProtocol { /** Event message for 'capabilities' event type. The event indicates that one or more capabilities have changed. - Since the capabilities are dependent on the frontend and its UI, it might not be possible to change that at random times (or too late). - Consequently this event has a hint characteristic: a frontend can only be expected to make a 'best effort' in honouring individual capabilities but there are no guarantees. + Since the capabilities are dependent on the client and its UI, it might not be possible to change that at random times (or too late). + Consequently this event has a hint characteristic: a client can only be expected to make a 'best effort' in honouring individual capabilities but there are no guarantees. Only changed capabilities need to be included, all other capabilities keep their values. */ interface CapabilitiesEvent extends Event { @@ -324,8 +323,7 @@ declare module DebugProtocol { } /** Event message for 'progressStart' event type. - The event signals that a long running operation is about to start and - provides additional information for the client to set up a corresponding progress and cancellation UI. + The event signals that a long running operation is about to start and provides additional information for the client to set up a corresponding progress and cancellation UI. The client is free to delay the showing of the UI in order to reduce flicker. This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ @@ -338,12 +336,11 @@ declare module DebugProtocol { progressId: string; /** Mandatory (short) title of the progress reporting. Shown in the UI to describe the long running operation. */ title: string; - /** The request ID that this progress report is related to. If specified a debug adapter is expected to emit - progress events for the long running request until the request has been either completed or cancelled. + /** The request ID that this progress report is related to. If specified a debug adapter is expected to emit progress events for the long running request until the request has been either completed or cancelled. If the request ID is omitted, the progress report is assumed to be related to some general activity of the debug adapter. */ requestId?: number; - /** If true, the request that reports progress may be canceled with a 'cancel' request. + /** If true, the request that reports progress may be cancelled with a 'cancel' request. So this property basically controls whether the client should use UX that supports cancellation. Clients that don't support cancellation are allowed to ignore the setting. */ @@ -356,7 +353,7 @@ declare module DebugProtocol { } /** Event message for 'progressUpdate' event type. - The event signals that the progress reporting needs to updated with a new message and/or percentage. + The event signals that the progress reporting needs to be updated with a new message and/or percentage. The client does not have to update the UI immediately, but the clients needs to keep track of the message and/or percentage values. This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ @@ -394,7 +391,7 @@ declare module DebugProtocol { interface InvalidatedEvent extends Event { // event: 'invalidated'; body: { - /** Optional set of logical areas that got invalidated. This property has a hint characteristic: a client can only be expected to make a 'best effort' in honouring the areas but there are no guarantees. If this property is missing, empty, or if values are not understand the client should assume a single value 'all'. */ + /** Optional set of logical areas that got invalidated. This property has a hint characteristic: a client can only be expected to make a 'best effort' in honouring the areas but there are no guarantees. If this property is missing, empty, or if values are not understood, the client should assume a single value 'all'. */ areas?: InvalidatedAreas[]; /** If specified, the client only needs to refetch data related to this thread. */ threadId?: number; @@ -406,7 +403,7 @@ declare module DebugProtocol { /** Event message for 'memory' event type. This event indicates that some memory range has been updated. It should only be sent if the debug adapter has received a value true for the `supportsMemoryEvent` capability of the `initialize` request. Clients typically react to the event by re-issuing a `readMemory` request if they show the memory identified by the `memoryReference` and if the updated memory range overlaps the displayed range. Clients should not make assumptions how individual memory references relate to each other, so they should not assume that they are part of a single continuous address range and might overlap. - Debug adapters can use this event to indicate that the contents of a memory range has changed due to some other DAP request like `setVariable` or `setExpression`. Debug adapters are not expected to emit this event for each and every memory change of a running program, because that information is typically not available from debuggers and it would flood clients with too many events. + Debug adapters can use this event to indicate that the contents of a memory range has changed due to some other request like `setVariable` or `setExpression`. Debug adapters are not expected to emit this event for each and every memory change of a running program, because that information is typically not available from debuggers and it would flood clients with too many events. */ interface MemoryEvent extends Event { // event: 'memory'; @@ -455,8 +452,7 @@ declare module DebugProtocol { } /** Initialize request; value of command field is 'initialize'. - The 'initialize' request is sent as the first request from the client to the debug adapter - in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. + The 'initialize' request is sent as the first request from the client to the debug adapter in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. The 'initialize' request may only be sent once. @@ -468,13 +464,13 @@ declare module DebugProtocol { /** Arguments for 'initialize' request. */ interface InitializeRequestArguments { - /** The ID of the (frontend) client using this adapter. */ + /** The ID of the client using this adapter. */ clientID?: string; - /** The human readable name of the (frontend) client using this adapter. */ + /** The human-readable name of the client using this adapter. */ clientName?: string; /** The ID of the debug adapter. */ adapterID: string; - /** The ISO-639 locale of the (frontend) client using this adapter, e.g. en-US or de-CH. */ + /** The ISO-639 locale of the client using this adapter, e.g. en-US or de-CH. */ locale?: string; /** If true all line numbers are 1-based (default). */ linesStartAt1?: boolean; @@ -535,7 +531,7 @@ declare module DebugProtocol { /** Arguments for 'launch' request. Additional attributes are implementation specific. */ interface LaunchRequestArguments { - /** If noDebug is true the launch request should launch the program without enabling debugging. */ + /** If noDebug is true, the launch request should launch the program without enabling debugging. */ noDebug?: boolean; /** Optional data from the previous, restarted session. The data is sent as the 'restart' attribute of the 'terminated' event. @@ -685,7 +681,7 @@ declare module DebugProtocol { /** Arguments for 'setBreakpoints' request. */ interface SetBreakpointsArguments { - /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ + /** The source location of the breakpoints; either 'source.path' or 'source.sourceReference' must be specified. */ source: Source; /** The code locations of the breakpoints. */ breakpoints?: SourceBreakpoint[]; @@ -738,7 +734,7 @@ declare module DebugProtocol { } /** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'. - The request configures the debuggers response to thrown exceptions. + The request configures the debugger's response to thrown exceptions. If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). Clients should only call this request if the capability 'exceptionBreakpointFilters' returns one or more filters. */ @@ -799,7 +795,7 @@ declare module DebugProtocol { dataId: string | null; /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ description: string; - /** Optional attribute listing the available access types for a potential data breakpoint. A UI frontend could surface this information. */ + /** Optional attribute listing the available access types for a potential data breakpoint. A UI client could surface this information. */ accessTypes?: DataBreakpointAccessType[]; /** Optional attribute indicating that a potential data breakpoint could be persisted across sessions. */ canPersist?: boolean; @@ -834,7 +830,7 @@ declare module DebugProtocol { } /** SetInstructionBreakpoints request; value of command field is 'setInstructionBreakpoints'. - Replaces all existing instruction breakpoints. Typically, instruction breakpoints would be set from a diassembly window. + Replaces all existing instruction breakpoints. Typically, instruction breakpoints would be set from a disassembly window. To clear all instruction breakpoints, specify an empty array. When an instruction breakpoint is hit, a 'stopped' event (with reason 'instruction breakpoint') is generated. Clients should only call this request if the capability 'supportsInstructionBreakpoints' is true. @@ -859,7 +855,7 @@ declare module DebugProtocol { } /** Continue request; value of command field is 'continue'. - The request resumes execution of all threads. If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true resumes only the specified thread. If not all threads were resumed, the 'allThreadsContinued' attribute of the response must be set to false. + The request resumes execution of all threads. If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true resumes only the specified thread. If not all threads were resumed, the 'allThreadsContinued' attribute of the response must be set to false. */ interface ContinueRequest extends Request { // command: 'continue'; @@ -884,7 +880,7 @@ declare module DebugProtocol { /** Next request; value of command field is 'next'. The request executes one step (in the given granularity) for the specified thread and allows all other threads to run freely by resuming them. - If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true prevents other suspended threads from resuming. + If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true prevents other suspended threads from resuming. The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. */ interface NextRequest extends Request { @@ -908,7 +904,7 @@ declare module DebugProtocol { /** StepIn request; value of command field is 'stepIn'. The request resumes the given thread to step into a function/method and allows all other threads to run freely by resuming them. - If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true prevents other suspended threads from resuming. + If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true prevents other suspended threads from resuming. If the request cannot step into a target, 'stepIn' behaves like the 'next' request. The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. If there are multiple function/method calls (or other targets) on the source line, @@ -938,7 +934,7 @@ declare module DebugProtocol { /** StepOut request; value of command field is 'stepOut'. The request resumes the given thread to step out (return) from a function/method and allows all other threads to run freely by resuming them. - If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true prevents other suspended threads from resuming. + If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true prevents other suspended threads from resuming. The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. */ interface StepOutRequest extends Request { @@ -962,7 +958,7 @@ declare module DebugProtocol { /** StepBack request; value of command field is 'stepBack'. The request executes one backward step (in the given granularity) for the specified thread and allows all other threads to run backward freely by resuming them. - If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true prevents other suspended threads from resuming. + If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true prevents other suspended threads from resuming. The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. Clients should only call this request if the capability 'supportsStepBack' is true. */ @@ -986,7 +982,7 @@ declare module DebugProtocol { } /** ReverseContinue request; value of command field is 'reverseContinue'. - The request resumes backward execution of all threads. If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true resumes only the specified thread. If not all threads were resumed, the 'allThreadsContinued' attribute of the response must be set to false. + The request resumes backward execution of all threads. If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true resumes only the specified thread. If not all threads were resumed, the 'allThreadsContinued' attribute of the response must be set to false. Clients should only call this request if the capability 'supportsStepBack' is true. */ interface ReverseContinueRequest extends Request { @@ -1028,7 +1024,7 @@ declare module DebugProtocol { /** Goto request; value of command field is 'goto'. The request sets the location where the debuggee will continue to run. - This makes it possible to skip the execution of code or to executed code again. + This makes it possible to skip the execution of code or to execute code again. The code between the current location and the goto target is not executed but skipped. The debug adapter first sends the response and then a 'stopped' event with reason 'goto'. Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true (because only then goto targets exist that can be passed as arguments). @@ -1071,7 +1067,7 @@ declare module DebugProtocol { /** StackTrace request; value of command field is 'stackTrace'. The request returns a stacktrace from the current execution state of a given thread. - A client can request all stack frames by omitting the startFrame and levels arguments. For performance conscious clients and if the debug adapter's 'supportsDelayedStackTraceLoading' capability is true, stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive less frames than requested, which is an indication that the end of the stack has been reached. + A client can request all stack frames by omitting the startFrame and levels arguments. For performance-conscious clients and if the debug adapter's 'supportsDelayedStackTraceLoading' capability is true, stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive fewer frames than requested, which is an indication that the end of the stack has been reached. */ interface StackTraceRequest extends Request { // command: 'stackTrace'; @@ -1227,7 +1223,7 @@ declare module DebugProtocol { body: { /** Content of the source reference. */ content: string; - /** Optional content type (mime type) of the source. */ + /** Optional content type (MIME type) of the source. */ mimeType?: string; }; } @@ -1315,7 +1311,7 @@ declare module DebugProtocol { } /** Evaluate request; value of command field is 'evaluate'. - Evaluates the given expression in the context of the top most stack frame. + Evaluates the given expression in the context of the topmost stack frame. The expression has access to any variables and arguments that are in scope. */ interface EvaluateRequest extends Request { @@ -1356,7 +1352,7 @@ declare module DebugProtocol { This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. */ type?: string; - /** Properties of a evaluate result that can be used to determine how to render the result in the UI. */ + /** Properties of an evaluate result that can be used to determine how to render the result in the UI. */ presentationHint?: VariablePresentationHint; /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31-1). @@ -1767,8 +1763,7 @@ declare module DebugProtocol { Additional attributes can be added to the module. They will show up in the module View if they have a corresponding ColumnDescriptor. - To avoid an unnecessary proliferation of additional attributes with similar semantics but different names - we recommend to re-use attributes from the 'recommended' list below first, and only introduce new attributes if nothing appropriate could be found. + To avoid an unnecessary proliferation of additional attributes with similar semantics but different names, we recommend to re-use attributes from the 'recommended' list below first, and only introduce new attributes if nothing appropriate could be found. */ interface Module { /** Unique identifier for the module. */ @@ -1787,7 +1782,7 @@ declare module DebugProtocol { isUserCode?: boolean; /** Version of Module. */ version?: string; - /** User understandable description of if symbols were found for the module (ex: 'Symbols Loaded', 'Symbols not found', etc. */ + /** User-understandable description of if symbols were found for the module (ex: 'Symbols Loaded', 'Symbols not found', etc. */ symbolStatus?: string; /** Logical full path to the symbol file. The exact definition is implementation defined. */ symbolFilePath?: string; @@ -1825,7 +1820,7 @@ declare module DebugProtocol { interface Thread { /** Unique identifier for the thread. */ id: number; - /** A name of the thread. */ + /** The name of the thread. */ name: string; } @@ -1940,7 +1935,7 @@ declare module DebugProtocol { name: string; /** The variable's value. This can be a multi-line text, e.g. for a function the body of a function. - For structured variables (which do not have a simple value), it is recommended to provide a one line representation of the structured object. This helps to identify the structured object in the collapsed state when its children are not yet visible. + For structured variables (which do not have a simple value), it is recommended to provide a one-line representation of the structured object. This helps to identify the structured object in the collapsed state when its children are not yet visible. An empty string can be used if no value should be shown in the UI. */ value: string; @@ -1981,8 +1976,7 @@ declare module DebugProtocol { 'innerClass': Indicates that the object is an inner class. 'interface': Indicates that the object is an interface. 'mostDerivedClass': Indicates that the object is the most derived class. - 'virtual': Indicates that the object is virtual, that means it is a synthetic object introducedby the - adapter for rendering purposes, e.g. an index range for large arrays. + 'virtual': Indicates that the object is virtual, that means it is a synthetic object introduced by the adapter for rendering purposes, e.g. an index range for large arrays. 'dataBreakpoint': Deprecated: Indicates that a data breakpoint is registered for the object. The 'hasDataBreakpoint' attribute should generally be used instead. etc. */ @@ -2034,11 +2028,11 @@ declare module DebugProtocol { */ condition?: string; /** An optional expression that controls how many hits of the breakpoint are ignored. - The backend is expected to interpret the expression as needed. + The debug adapter is expected to interpret the expression as needed. The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. */ hitCondition?: string; - /** If this attribute exists and is non-empty, the backend must not 'break' (stop) + /** If this attribute exists and is non-empty, the debug adapter must not 'break' (stop) but log the message instead. Expressions within {} are interpolated. The attribute is only honored by a debug adapter if the capability 'supportsLogPoints' is true. */ @@ -2054,7 +2048,7 @@ declare module DebugProtocol { */ condition?: string; /** An optional expression that controls how many hits of the breakpoint are ignored. - The backend is expected to interpret the expression as needed. + The debug adapter is expected to interpret the expression as needed. The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. */ hitCondition?: string; @@ -2072,7 +2066,7 @@ declare module DebugProtocol { /** An optional expression for conditional breakpoints. */ condition?: string; /** An optional expression that controls how many hits of the breakpoint are ignored. - The backend is expected to interpret the expression as needed. + The debug adapter is expected to interpret the expression as needed. */ hitCondition?: string; } @@ -2092,7 +2086,7 @@ declare module DebugProtocol { */ condition?: string; /** An optional expression that controls how many hits of the breakpoint are ignored. - The backend is expected to interpret the expression as needed. + The debug adapter is expected to interpret the expression as needed. The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. */ hitCondition?: string; @@ -2102,7 +2096,7 @@ declare module DebugProtocol { interface Breakpoint { /** An optional identifier for the breakpoint. It is needed if breakpoint events are used to update or remove breakpoints. */ id?: number; - /** If true breakpoint could be set (but not necessarily at the desired location). */ + /** If true, the breakpoint could be set (but not necessarily at the desired location). */ verified: boolean; /** An optional message about the state of the breakpoint. This is shown to the user and can be used to explain why a breakpoint could not be verified. @@ -2143,6 +2137,14 @@ declare module DebugProtocol { id: number; /** The name of the stepIn target (shown in the UI). */ label: string; + /** An optional line of the step in target. */ + line?: number; + /** An optional column of the step in target. */ + column?: number; + /** An optional end line of the range covered by the step in target. */ + endLine?: number; + /** An optional end column of the range covered by the step in target. */ + endColumn?: number; } /** A GotoTarget describes a code location that can be used as a target in the 'goto' request. @@ -2264,8 +2266,7 @@ declare module DebugProtocol { type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. - If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing or - it matches anything except the names provided if 'negate' is true. + If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing, or it matches anything except the names provided if 'negate' is true. */ interface ExceptionPathSegment { /** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */ @@ -2325,4 +2326,3 @@ declare module DebugProtocol { */ type InvalidatedAreas = 'all' | 'stacks' | 'threads' | 'variables' | string; } - diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index a2a9c7f5480..26575ff3b88 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -72,6 +72,11 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE type: 'string', default: '' }, + deprecated: { + description: nls.localize('vscode.extension.contributes.debuggers.deprecated', "Optional message to mark this debug type as being deprecated."), + type: 'string', + default: '' + }, windows: { description: nls.localize('vscode.extension.contributes.debuggers.windows', "Windows specific settings."), type: 'object', diff --git a/src/vs/workbench/contrib/debug/common/debugTelemetry.ts b/src/vs/workbench/contrib/debug/common/debugTelemetry.ts index a68ce7e798e..a39c0651c55 100644 --- a/src/vs/workbench/contrib/debug/common/debugTelemetry.ts +++ b/src/vs/workbench/contrib/debug/common/debugTelemetry.ts @@ -18,6 +18,7 @@ export class DebugTelemetry { const extension = dbgr.getMainExtensionDescriptor(); /* __GDPR__ "debugSessionStart" : { + "owner": "connor4312", "type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "breakpointCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "exceptionBreakpoints": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -44,6 +45,7 @@ export class DebugTelemetry { /* __GDPR__ "debugSessionStop" : { + "owner": "connor4312", "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "success": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "sessionLengthInSeconds": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 6b58a993556..66325b37e09 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -260,9 +260,7 @@ function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePath: (toDA: case 'disassemble': { const di = <DebugProtocol.DisassembleResponse>response; - if (di.body) { - di.body.instructions.forEach(di => fixSourcePath(false, di.location)); - } + di.body?.instructions.forEach(di => fixSourcePath(false, di.location)); } break; default: diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 531f7f789cc..f7041e2099a 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { isObject } from 'vs/base/common/types'; import { IJSONSchema, IJSONSchemaMap, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IConfig, IDebuggerContribution, IDebugAdapter, IDebugger, IDebugSession, IAdapterManager, IDebugService, debuggerDisabledMessage } from 'vs/workbench/contrib/debug/common/debug'; +import { IConfig, IDebuggerContribution, IDebugAdapter, IDebugger, IDebugSession, IAdapterManager, IDebugService, debuggerDisabledMessage, IDebuggerMetadata } from 'vs/workbench/contrib/debug/common/debug'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import * as ConfigurationResolverUtils from 'vs/workbench/services/configurationResolver/common/configurationResolverUtils'; @@ -21,7 +21,7 @@ import { cleanRemoteAuthority } from 'vs/platform/telemetry/common/telemetryUtil import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -export class Debugger implements IDebugger { +export class Debugger implements IDebugger, IDebuggerMetadata { private debuggerContribution: IDebuggerContribution; private mergedExtensionDescriptions: IExtensionDescription[] = []; @@ -147,6 +147,14 @@ export class Debugger implements IDebugger { return !this.debuggerWhen || this.contextKeyService.contextMatchesRules(this.debuggerWhen); } + get uiMessages() { + return this.debuggerContribution.uiMessages; + } + + interestedInLanguage(languageId: string): boolean { + return !!(this.languages && this.languages.indexOf(languageId) >= 0); + } + hasInitialConfiguration(): boolean { return !!this.debuggerContribution.initialConfigurations; } @@ -228,7 +236,8 @@ export class Debugger implements IDebugger { enumDescriptions: [this.label], description: nls.localize('debugType', "Type of configuration."), pattern: '^(?!node2)', - deprecationMessage: this.enabled ? undefined : debuggerDisabledMessage(this.type), + deprecationMessage: this.debuggerContribution.deprecated || (this.enabled ? undefined : debuggerDisabledMessage(this.type)), + doNotSuggest: !!this.debuggerContribution.deprecated, errorMessage: nls.localize('debugTypeNotRecognised', "The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled."), patternErrorMessage: nls.localize('node2NotSupported', "\"node2\" is no longer supported, use \"node\" instead and set the \"protocol\" attribute to \"inspector\".") }; diff --git a/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts b/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts new file mode 100644 index 00000000000..51218a3475d --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IDebugService, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +import { dirname } from 'vs/base/common/resources'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; + + +interface IPickerLoadedScriptItem extends IQuickPickItem { + accept(): void; +} + + +/** + * This function takes a regular quickpick and makes one for loaded scripts that has persistent headers + * e.g. when some picks are filtered out, the ones that are visible still have its header. + */ +export async function showLoadedScriptMenu(accessor: ServicesAccessor) { + const quickInputService = accessor.get(IQuickInputService); + const debugService = accessor.get(IDebugService); + const editorService = accessor.get(IEditorService); + const sessions = debugService.getModel().getSessions(false); + const modelService = accessor.get(IModelService); + const languageService = accessor.get(ILanguageService); + const labelService = accessor.get(ILabelService); + + const localDisposableStore = new DisposableStore(); + const quickPick = quickInputService.createQuickPick<IPickerLoadedScriptItem>(); + localDisposableStore.add(quickPick); + quickPick.matchOnLabel = quickPick.matchOnDescription = quickPick.matchOnDetail = quickPick.sortByLabel = false; + quickPick.placeholder = nls.localize('moveFocusedView.selectView', "Search loaded scripts by name"); + quickPick.items = await _getPicks(quickPick.value, sessions, editorService, modelService, languageService, labelService); + + localDisposableStore.add(quickPick.onDidChangeValue(async () => { + quickPick.items = await _getPicks(quickPick.value, sessions, editorService, modelService, languageService, labelService); + })); + localDisposableStore.add(quickPick.onDidAccept(() => { + const selectedItem = quickPick.selectedItems[0]; + selectedItem.accept(); + quickPick.hide(); + localDisposableStore.dispose(); + })); + quickPick.show(); +} + +async function _getPicksFromSession(session: IDebugSession, filter: string, editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): Promise<Array<IPickerLoadedScriptItem | IQuickPickSeparator>> { + const items: Array<IPickerLoadedScriptItem | IQuickPickSeparator> = []; + items.push({ type: 'separator', label: session.name }); + const sources = await session.getLoadedSources(); + + sources.forEach((element: Source) => { + const pick = _createPick(element, filter, editorService, modelService, languageService, labelService); + if (pick) { + items.push(pick); + } + + }); + return items; +} +async function _getPicks(filter: string, sessions: IDebugSession[], editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): Promise<Array<IPickerLoadedScriptItem | IQuickPickSeparator>> { + const loadedScriptPicks: Array<IPickerLoadedScriptItem | IQuickPickSeparator> = []; + + + const picks = await Promise.all( + sessions.map((session) => _getPicksFromSession(session, filter, editorService, modelService, languageService, labelService)) + ); + + for (const row of picks) { + for (const elem of row) { + loadedScriptPicks.push(elem); + } + } + return loadedScriptPicks; +} + +function _createPick(source: Source, filter: string, editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): IPickerLoadedScriptItem | undefined { + + const label = labelService.getUriBasenameLabel(source.uri); + const desc = labelService.getUriLabel(dirname(source.uri)); + + // manually filter so that headers don't get filtered out + const labelHighlights = matchesFuzzy(filter, label, true); + const descHighlights = matchesFuzzy(filter, desc, true); + if (labelHighlights || descHighlights) { + return { + label, + description: desc === '.' ? undefined : desc, + highlights: { label: labelHighlights ?? undefined, description: descHighlights ?? undefined }, + iconClasses: getIconClasses(modelService, languageService, source.uri), + accept: () => { + if (source.available) { + source.openInEditor(editorService, { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 }); + } + } + }; + } + return undefined; +} diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 4f0bf897787..b4f6c7e4daf 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -259,9 +259,7 @@ export class ExecutableDebugAdapter extends StreamDebugAdapter { // }); this.serverProcess.stderr!.on('data', (data: string) => { const channel = outputService.getChannel(ExtensionsChannelId); - if (channel) { - channel.append(sanitize(data)); - } + channel?.append(sanitize(data)); }); } else { this.serverProcess.stderr!.resume(); diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 465d92947c5..3e9f0e81234 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -161,7 +161,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? case ShellType.bash: { quote = (s: string) => { - s = s.replace(/(["'\\\$!><#()\[\]*&^| ;])/g, '\\$1'); + s = s.replace(/(["'\\\$!><#()\[\]*&^| ;{}`])/g, '\\$1'); return s.length === 0 ? `""` : s; }; diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index ef968b98f88..ab592668f64 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -95,7 +95,7 @@ suite('Debug - Base Debug View', () => { let name = $('.'); let value = $('.'); const label = new HighlightedLabel(name); - let lazyButton = $('.'); + const lazyButton = $('.'); renderVariable(variable, { expression, name, value, label, lazyButton }, false, []); assert.strictEqual(label.element.textContent, 'foo'); diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index e8d88ddaf01..8dd479109dd 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -7,15 +7,18 @@ import * as assert from 'assert'; import { URI as uri } from 'vs/base/common/uri'; import { DebugModel, Breakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { getExpandedBodySize, getBreakpointMessageAndIcon } from 'vs/workbench/contrib/debug/browser/breakpointsView'; -import { dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; -import { IBreakpointData, IBreakpointUpdateData, State } from 'vs/workbench/contrib/debug/common/debug'; +import { IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { OverviewRulerLane } from 'vs/editor/common/model'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; -import { createMockDebugModel } from 'vs/workbench/contrib/debug/test/browser/mockDebug'; +import { createMockDebugModel, MockDebugService } from 'vs/workbench/contrib/debug/test/browser/mockDebug'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { LanguageService } from 'vs/editor/common/services/languageService'; function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakpointData[]): void { let eventCount = 0; @@ -39,11 +42,16 @@ function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakp suite('Debug - Breakpoints', () => { let model: DebugModel; + const disposables = new DisposableStore(); setup(() => { model = createMockDebugModel(); }); + teardown(() => { + disposables.clear(); + }); + // Breakpoints test('simple', () => { @@ -350,7 +358,10 @@ suite('Debug - Breakpoints', () => { ]); const breakpoints = model.getBreakpoints(); - let decorations = createBreakpointDecorations(textModel, breakpoints, State.Running, true, true); + const instantiationService = new TestInstantiationService(); + instantiationService.stub(IDebugService, new MockDebugService()); + instantiationService.stub(ILanguageService, disposables.add(new LanguageService())); + let decorations = instantiationService.invokeFunction(accessor => createBreakpointDecorations(accessor, textModel, breakpoints, State.Running, true, true)); assert.strictEqual(decorations.length, 3); // last breakpoint filtered out since it has a large line number assert.deepStrictEqual(decorations[0].range, new Range(1, 1, 1, 2)); assert.deepStrictEqual(decorations[1].range, new Range(2, 4, 2, 5)); @@ -358,10 +369,10 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(decorations[0].options.beforeContentClassName, undefined); assert.strictEqual(decorations[1].options.before?.inlineClassName, `debug-breakpoint-placeholder`); assert.strictEqual(decorations[0].options.overviewRuler?.position, OverviewRulerLane.Left); - const expected = new MarkdownString().appendCodeblock(languageId, 'Expression condition: x > 5'); + const expected = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendCodeblock(languageId, 'Expression condition: x > 5'); assert.deepStrictEqual(decorations[0].options.glyphMarginHoverMessage, expected); - decorations = createBreakpointDecorations(textModel, breakpoints, State.Running, true, false); + decorations = instantiationService.invokeFunction(accessor => createBreakpointDecorations(accessor, textModel, breakpoints, State.Running, true, false)); assert.strictEqual(decorations[0].options.overviewRuler, null); textModel.dispose(); diff --git a/src/vs/workbench/contrib/debug/test/node/terminals.test.ts b/src/vs/workbench/contrib/debug/test/node/terminals.test.ts index cbb780f76de..71abb2e2b89 100644 --- a/src/vs/workbench/contrib/debug/test/node/terminals.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/terminals.test.ts @@ -11,7 +11,7 @@ suite('Debug - prepareCommand', () => { test('bash', () => { assert.strictEqual( prepareCommand('bash', ['{$} (']).trim(), - '{\\$}\\ \\('); + '\\{\\$\\}\\ \\('); assert.strictEqual( prepareCommand('bash', ['hello', 'world', '--flag=true']).trim(), 'hello world --flag=true'); diff --git a/src/vs/workbench/contrib/emmet/browser/emmetActions.ts b/src/vs/workbench/contrib/emmet/browser/emmetActions.ts index 08b8e4d9c32..95bbfe3d823 100644 --- a/src/vs/workbench/contrib/emmet/browser/emmetActions.ts +++ b/src/vs/workbench/contrib/emmet/browser/emmetActions.ts @@ -101,12 +101,12 @@ export abstract class EmmetEditorAction extends EditorAction { return null; } - let checkParentMode = (): string => { - let languageGrammar = grammars.getGrammar(syntax); + const checkParentMode = (): string => { + const languageGrammar = grammars.getGrammar(syntax); if (!languageGrammar) { return syntax; } - let languages = languageGrammar.split('.'); + const languages = languageGrammar.split('.'); if (languages.length < 2) { return syntax; } 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 fdcf9b5c181..41dc18cf8f2 100644 --- a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -40,7 +40,7 @@ suite('Emmet', () => { } model.setMode(mode); - let langOutput = EmmetEditorAction.getLanguage(editor, new MockGrammarContributions(scopeName)); + const langOutput = EmmetEditorAction.getLanguage(editor, new MockGrammarContributions(scopeName)); if (!langOutput) { assert.fail('langOutput not found'); } diff --git a/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts b/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts index a850ee85522..a10722cff13 100644 --- a/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts +++ b/src/vs/workbench/contrib/experiments/browser/experimentalPrompt.ts @@ -58,9 +58,7 @@ export class ExperimentalPrompts extends Disposable implements IWorkbenchContrib this.paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true) .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) .then(viewlet => { - if (viewlet) { - viewlet.search('curated:' + command.curatedExtensionsKey); - } + viewlet?.search('curated:' + command.curatedExtensionsKey); }); } else if (command.codeCommand) { this.commandService.executeCommand(command.codeCommand.id, ...command.codeCommand.arguments); diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index 0770dbb143e..235217dbc64 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -223,9 +223,9 @@ export class ExperimentService extends Disposable implements IExperimentService public markAsCompleted(experimentId: string): void { const storageKey = 'experiments.' + experimentId; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), {}); experimentState.state = ExperimentState.Complete; - this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.APPLICATION, StorageTarget.MACHINE); } protected async getExperiments(): Promise<IRawExperiment[] | null> { @@ -255,11 +255,11 @@ export class ExperimentService extends Disposable implements IExperimentService return this.getExperiments().then(rawExperiments => { // Offline mode if (!rawExperiments) { - const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); + const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.APPLICATION), []); if (Array.isArray(allExperimentIdsFromStorage)) { allExperimentIdsFromStorage.forEach(experimentId => { const storageKey = 'experiments.' + experimentId; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), null); + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), null); if (experimentState) { this._experiments.push({ id: experimentId, @@ -278,19 +278,19 @@ export class ExperimentService extends Disposable implements IExperimentService rawExperiments = rawExperiments.filter(e => (e.schemaVersion || 0) <= currentSchemaVersion); // Clear disbaled/deleted experiments from storage - const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.GLOBAL), []); + const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.APPLICATION), []); const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase()); if (Array.isArray(allExperimentIdsFromStorage)) { allExperimentIdsFromStorage.forEach(experiment => { if (enabledExperiments.indexOf(experiment) === -1) { - this.storageService.remove(`experiments.${experiment}`, StorageScope.GLOBAL); + this.storageService.remove(`experiments.${experiment}`, StorageScope.APPLICATION); } }); } if (enabledExperiments.length) { - this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.APPLICATION, StorageTarget.MACHINE); } else { - this.storageService.remove('allExperiments', StorageScope.GLOBAL); + this.storageService.remove('allExperiments', StorageScope.APPLICATION); } const activationEvents = new Set(rawExperiments.map(exp => exp.condition?.activationEvent?.event) @@ -306,6 +306,7 @@ export class ExperimentService extends Disposable implements IExperimentService const promises = rawExperiments.map(experiment => this.evaluateExperiment(experiment)); return Promise.all(promises).then(() => { type ExperimentsClassification = { + owner: 'sbatten'; experiments: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; this.telemetryService.publicLog2<{ experiments: string[] }, ExperimentsClassification>('experiments', { experiments: this._experiments.map(e => e.id) }); @@ -347,7 +348,7 @@ export class ExperimentService extends Disposable implements IExperimentService } const storageKey = 'experiments.' + experiment.id; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), {}); if (!experimentState.hasOwnProperty('enabled')) { experimentState.enabled = processedExperiment.enabled; } @@ -359,7 +360,7 @@ export class ExperimentService extends Disposable implements IExperimentService return this.shouldRunExperiment(experiment, processedExperiment).then((state: ExperimentState) => { experimentState.state = processedExperiment.state = state; - this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.APPLICATION, StorageTarget.MACHINE); if (state === ExperimentState.Run) { this.fireRunExperiment(processedExperiment); @@ -371,7 +372,7 @@ export class ExperimentService extends Disposable implements IExperimentService private fireRunExperiment(experiment: IExperiment) { this._onExperimentEnabled.fire(experiment); - const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.APPLICATION), []); if (runExperimentIdsFromStorage.indexOf(experiment.id) === -1) { runExperimentIdsFromStorage.push(experiment.id); } @@ -379,14 +380,14 @@ export class ExperimentService extends Disposable implements IExperimentService // Ensure we dont store duplicates const distinctExperiments = distinct(runExperimentIdsFromStorage); if (runExperimentIdsFromStorage.length !== distinctExperiments.length) { - this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(distinctExperiments), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(distinctExperiments), StorageScope.APPLICATION, StorageTarget.MACHINE); } } private checkExperimentDependencies(experiment: IRawExperiment): boolean { const experimentsPreviouslyRun = experiment.condition?.experimentsPreviouslyRun; if (experimentsPreviouslyRun) { - const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.GLOBAL), []); + const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.APPLICATION), []); let includeCheck = true; let excludeCheck = true; const includes = experimentsPreviouslyRun.includes; @@ -406,9 +407,9 @@ export class ExperimentService extends Disposable implements IExperimentService private recordActivatedEvent(event: string) { const key = experimentEventStorageKey(event); - const record = getCurrentActivationRecord(safeParse(this.storageService.get(key, StorageScope.GLOBAL), undefined)); + const record = getCurrentActivationRecord(safeParse(this.storageService.get(key, StorageScope.APPLICATION), undefined)); record.count[0]++; - this.storageService.store(key, JSON.stringify(record), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(key, JSON.stringify(record), StorageScope.APPLICATION, StorageTarget.MACHINE); this._experiments .filter(e => { @@ -433,7 +434,7 @@ export class ExperimentService extends Disposable implements IExperimentService const events = typeof setting.event === 'string' ? [setting.event] : setting.event; for (const event of events) { - const { count } = getCurrentActivationRecord(safeParse(this.storageService.get(experimentEventStorageKey(event), StorageScope.GLOBAL), undefined)); + const { count } = getCurrentActivationRecord(safeParse(this.storageService.get(experimentEventStorageKey(event), StorageScope.APPLICATION), undefined)); for (const entry of count) { if (entry > 0) { @@ -482,7 +483,7 @@ export class ExperimentService extends Disposable implements IExperimentService return Promise.resolve(ExperimentState.NoRun); } - const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL); + const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION); if ((condition.newUser === true && !isNewUser) || (condition.newUser === false && isNewUser)) { return Promise.resolve(ExperimentState.NoRun); @@ -531,7 +532,7 @@ export class ExperimentService extends Disposable implements IExperimentService } const storageKey = 'experiments.' + experiment.id; - const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), {}); return extensionsCheckPromise.then(success => { const fileEdits = condition.fileEdits; @@ -548,7 +549,7 @@ export class ExperimentService extends Disposable implements IExperimentService // Process model-save event every 250ms to reduce load const onModelsSavedWorker = this._register(new RunOnceWorker<ITextFileEditorModel>(models => { const date = new Date().toDateString(); - const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); + const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), {}); if (latestExperimentState.state !== ExperimentState.Evaluating) { onSaveHandler.dispose(); onModelsSavedWorker.dispose(); @@ -578,12 +579,12 @@ export class ExperimentService extends Disposable implements IExperimentService if (filePathCheck && workspaceCheck) { latestExperimentState.editCount = (latestExperimentState.editCount || 0) + 1; latestExperimentState.lastEditedDate = date; - this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.APPLICATION, StorageTarget.MACHINE); } }); if (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) { processedExperiment.state = latestExperimentState.state = (typeof condition.userProbability === 'number' && Math.random() < condition.userProbability && this.checkExperimentDependencies(experiment)) ? ExperimentState.Run : ExperimentState.NoRun; - this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.APPLICATION, StorageTarget.MACHINE); if (latestExperimentState.state === ExperimentState.Run && processedExperiment.action && ExperimentActionType[processedExperiment.action.type] === ExperimentActionType.Prompt) { this.fireRunExperiment(processedExperiment); } diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index a97d0ace12a..2dbfdafd694 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -535,7 +535,7 @@ suite('Experiment Service', () => { didGetCall = true; assert.strictEqual(key, 'experimentEventRecord-my-event'); assert.deepStrictEqual(JSON.parse(value).count, [1, 0, 10, 0, 0, 0, 0]); - assert.strictEqual(scope, StorageScope.GLOBAL); + assert.strictEqual(scope, StorageScope.APPLICATION); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index ed27525eb03..c801cfe94f6 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -95,9 +95,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { protected async _updateExtensions(): Promise<void> { this._elements = await this._resolveExtensions(); - if (this._list) { - this._list.splice(0, this._list.length, this._elements); - } + this._list?.splice(0, this._list.length, this._elements); } private async _resolveExtensions(): Promise<IRuntimeExtension[]> { @@ -105,16 +103,16 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { const extensionsDescriptions = (await this._extensionService.getExtensions()).filter((extension) => { return Boolean(extension.main) || Boolean(extension.browser); }); - let marketplaceMap: { [id: string]: IExtension } = Object.create(null); + const marketplaceMap: { [id: string]: IExtension } = Object.create(null); const marketPlaceExtensions = await this._extensionsWorkbenchService.queryLocal(); - for (let extension of marketPlaceExtensions) { + for (const extension of marketPlaceExtensions) { marketplaceMap[ExtensionIdentifier.toKey(extension.identifier.id)] = extension; } - let statusMap = this._extensionService.getExtensionsStatus(); + const statusMap = this._extensionService.getExtensionsStatus(); // group profile segments by extension - let segments: { [id: string]: number[] } = Object.create(null); + const segments: { [id: string]: number[] } = Object.create(null); const profileInfo = this._getProfileInfo(); if (profileInfo) { @@ -141,7 +139,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { let extProfileInfo: IExtensionProfileInformation | null = null; if (profileInfo) { - let extensionSegments = segments[ExtensionIdentifier.toKey(extensionDescription.identifier)] || []; + const extensionSegments = segments[ExtensionIdentifier.toKey(extensionDescription.identifier)] || []; let extensionTotalTime = 0; for (let j = 0, lenJ = extensionSegments.length / 2; j < lenJ; j++) { const startTime = extensionSegments[2 * j]; @@ -279,7 +277,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { data.version.textContent = element.description.version; const activationTimes = element.status.activationTimes!; - let syncTime = activationTimes.codeLoadingTime + activationTimes.activateCallTime; + const syncTime = activationTimes.codeLoadingTime + activationTimes.activateCallTime; data.activationTime.textContent = activationTimes.activationReason.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`; data.actionbar.clear(); @@ -305,7 +303,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { ] }, "Activated by {0} on start-up", activationId); } else if (/^workspaceContains:/.test(activationEvent)) { - let fileNameOrGlob = activationEvent.substr('workspaceContains:'.length); + const fileNameOrGlob = activationEvent.substr('workspaceContains:'.length); if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0) { title = nls.localize({ key: 'workspaceContainsGlobActivation', @@ -340,7 +338,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { ] }, "Activated by {0} after start-up finished", activationId); } else if (/^onLanguage:/.test(activationEvent)) { - let language = activationEvent.substr('onLanguage:'.length); + const language = activationEvent.substr('onLanguage:'.length); title = nls.localize('languageActivation', "Activated by {1} because you opened a {0} file", language, activationId); } else { title = nls.localize({ @@ -475,9 +473,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } public layout(dimension: Dimension): void { - if (this._list) { - this._list.layout(dimension.height); - } + this._list?.layout(dimension.height); } protected abstract _getProfileInfo(): IExtensionHostProfile | null; diff --git a/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts index f287d1ebe38..1918926fef5 100644 --- a/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts @@ -16,8 +16,10 @@ import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRe import { localize } from 'vs/nls'; type DynamicWorkspaceRecommendationsClassification = { - count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - cache: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + owner: 'sandy081'; + comment: 'Information about recommendations by scanning the workspace'; + count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Total number of extensions those are recommended' }; + cache: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Flag if extensions are recommended from cache or not' }; }; type IStoredDynamicWorkspaceRecommendations = { recommendations: string[]; timestamp: number }; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 4cfe2517ef5..381096c914f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -6,30 +6,30 @@ import 'vs/css!./media/extensionEditor'; import { localize } from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; -import { OS } from 'vs/base/common/platform'; +import { OS, locale } from 'vs/base/common/platform'; import { Event, Emitter } from 'vs/base/common/event'; import { Cache, CacheResult } from 'vs/base/common/cache'; import { Action, IAction } from 'vs/base/common/actions'; import { getErrorMessage, isCancellationError, onUnexpectedError } from 'vs/base/common/errors'; -import { dispose, toDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { append, $, finalHandler, join, addDisposableListener, EventType, setParentFlowTo, reset, Dimension } from 'vs/base/browser/dom'; +import { dispose, toDisposable, Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { append, $, join, addDisposableListener, setParentFlowTo, reset, Dimension } from 'vs/base/browser/dom'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; +import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionManifest, IKeyBinding, IView, IViewContainer } from 'vs/platform/extensions/common/extensions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers, ExtensionEditorTab, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions'; -import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget, ExtensionWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtensionContainer } from 'vs/workbench/contrib/extensions/common/extensions'; +import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget, ExtensionWidget, ExtensionStatusWidget, ExtensionRecommendationWidget, SponsorWidget, onClick } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { UpdateAction, ReloadAction, EnableDropDownAction, DisableDropDownAction, ExtensionStatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, - InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction + InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, MigrateDeprecatedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -49,7 +49,6 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry'; import { isUndefined } from 'vs/base/common/types'; import { IWebviewService, IWebview, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { generateUuid } from 'vs/base/common/uuid'; import { platform } from 'vs/base/common/process'; import { URI } from 'vs/base/common/uri'; @@ -66,12 +65,11 @@ import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { errorIcon, infoIcon, preReleaseIcon, starEmptyIcon, verifiedPublisherIcon as verifiedPublisherThemeIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; -import { MarkdownString } from 'vs/base/common/htmlContent'; +import { errorIcon, infoIcon, preReleaseIcon, verifiedPublisherIcon as verifiedPublisherThemeIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IExtensionGalleryService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import semver = require('vs/base/common/semver/semver'); +import * as semver from 'vs/base/common/semver/semver'; class NavBar extends Disposable { @@ -147,8 +145,6 @@ interface IExtensionEditorTemplate { description: HTMLElement; actionsAndStatusContainer: HTMLElement; extensionActionBar: ActionBar; - status: HTMLElement; - recommendation: HTMLElement; navbar: NavBar; content: HTMLElement; header: HTMLElement; @@ -251,7 +247,6 @@ export class ExtensionEditor extends EditorPane { @INotificationService private readonly notificationService: INotificationService, @IOpenerService private readonly openerService: IOpenerService, @IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService, - @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, @IStorageService storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @IWebviewService private readonly webviewService: IWebviewService, @@ -310,12 +305,15 @@ export class ExtensionEditor extends EditorPane { rating.setAttribute('role', 'link'); // #132645 const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, rating, false); - const widgets = [ + const sponsorWidget = this.instantiationService.createInstance(SponsorWidget, append(subtitle, $('.subtitle-entry'))); + + const widgets: ExtensionWidget[] = [ remoteBadge, versionWidget, preReleaseWidget, installCountWidget, ratingsWidget, + sponsorWidget, ]; const description = append(details, $('.description')); @@ -338,6 +336,7 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [ [ + this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, false), this.instantiationService.createInstance(UninstallAction), this.instantiationService.createInstance(InstallAnotherVersionAction), ] @@ -371,14 +370,30 @@ export class ExtensionEditor extends EditorPane { extensionActionBar.setFocusable(true); })); - const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); - for (const disposable of [...actions, ...widgets, extensionContainers]) { + const otherExtensionContainers: IExtensionContainer[] = []; + const extensionStatusAction = this.instantiationService.createInstance(ExtensionStatusAction); + const extensionStatusWidget = this._register(this.instantiationService.createInstance(ExtensionStatusWidget, append(actionsAndStatusContainer, $('.status')), extensionStatusAction)); + + otherExtensionContainers.push(extensionStatusAction, new class extends ExtensionWidget { + render() { + actionsAndStatusContainer.classList.toggle('list-layout', this.extension?.state === ExtensionState.Installed); + } + }()); + + const recommendationWidget = this.instantiationService.createInstance(ExtensionRecommendationWidget, append(details, $('.recommendation'))); + widgets.push(recommendationWidget); + + this._register(Event.any(extensionStatusWidget.onDidRender, recommendationWidget.onDidRender)(() => { + if (this.dimension) { + this.layout(this.dimension); + } + })); + + const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, ...otherExtensionContainers]); + for (const disposable of [...actions, ...widgets, ...otherExtensionContainers, extensionContainers]) { this._register(disposable); } - const status = append(actionsAndStatusContainer, $('.status')); - const recommendation = append(details, $('.recommendation')); - this._register(Event.chain(extensionActionBar.onDidRun) .map(({ error }) => error) .filter(error => !!error) @@ -407,8 +422,6 @@ export class ExtensionEditor extends EditorPane { rating, actionsAndStatusContainer, extensionActionBar, - status, - recommendation, set extension(extension: IExtension) { extensionContainers.extension = extension; }, @@ -422,20 +435,6 @@ export class ExtensionEditor extends EditorPane { }; } - private onClick(element: HTMLElement, callback: () => void): IDisposable { - const disposables: DisposableStore = new DisposableStore(); - disposables.add(addDisposableListener(element, EventType.CLICK, finalHandler(callback))); - disposables.add(addDisposableListener(element, EventType.KEY_UP, e => { - const keyboardEvent = new StandardKeyboardEvent(e); - if (keyboardEvent.equals(KeyCode.Space) || keyboardEvent.equals(KeyCode.Enter)) { - e.preventDefault(); - e.stopPropagation(); - callback(); - } - })); - return disposables; - } - override async setInput(input: ExtensionsInput, options: IExtensionEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { await super.setInput(input, options, context, token); this.updatePreReleaseVersionContext(); @@ -514,6 +513,7 @@ export class ExtensionEditor extends EditorPane { template.name.textContent = extension.displayName; template.name.classList.toggle('clickable', !!extension.url); + template.name.classList.toggle('deprecated', !!extension.deprecationInfo); template.preview.style.display = extension.preview ? 'inherit' : 'none'; template.builtin.style.display = extension.isBuiltin ? 'inherit' : 'none'; @@ -530,18 +530,15 @@ export class ExtensionEditor extends EditorPane { template.rating.classList.toggle('clickable', !!extension.url); if (extension.url) { - this.transientDisposables.add(this.onClick(template.name, () => this.openerService.open(URI.parse(extension.url!)))); - this.transientDisposables.add(this.onClick(template.rating, () => this.openerService.open(URI.parse(`${extension.url}&ssr=false#review-details`)))); - this.transientDisposables.add(this.onClick(template.publisher, () => { + this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(extension.url!)))); + this.transientDisposables.add(onClick(template.rating, () => this.openerService.open(URI.parse(`${extension.url}&ssr=false#review-details`)))); + this.transientDisposables.add(onClick(template.publisher, () => { this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) .then(viewlet => viewlet.search(`publisher:"${extension.publisherDisplayName}"`)); })); } - this.setStatus(extension, template); - this.setRecommendationText(extension, template); - const manifest = await this.extensionManifest.get().promise; if (token.isCancellationRequested) { return; @@ -561,6 +558,7 @@ export class ExtensionEditor extends EditorPane { } /* __GDPR__ "extensionGallery:openExtension" : { + "owner": "sandy081", "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "${include}": [ "${GalleryExtensionTelemetryData}" @@ -614,71 +612,6 @@ export class ExtensionEditor extends EditorPane { template.navbar.onChange(e => this.onNavbarChange(extension, e, template), this, this.transientDisposables); } - private setStatus(extension: IExtension, template: IExtensionEditorTemplate): void { - const disposables = this.transientDisposables.add(new DisposableStore()); - const extensionStatus = disposables.add(this.instantiationService.createInstance(ExtensionStatusAction)); - extensionStatus.extension = extension; - const updateStatus = (layout: boolean) => { - disposables.clear(); - reset(template.status); - const status = extensionStatus.status; - if (status) { - if (status.icon) { - const statusIconActionBar = disposables.add(new ActionBar(template.status, { animated: false })); - statusIconActionBar.push(extensionStatus, { icon: true, label: false }); - } - disposables.add(this.renderMarkdownText(status.message.value, append(template.status, $('.status-text')))); - } - if (layout && this.dimension) { - this.layout(this.dimension); - } - }; - updateStatus(false); - this.transientDisposables.add(extensionStatus.onDidChangeStatus(() => updateStatus(true))); - - const updateActionLayout = () => template.actionsAndStatusContainer.classList.toggle('list-layout', extension.state === ExtensionState.Installed); - updateActionLayout(); - this.transientDisposables.add(this.extensionsWorkbenchService.onChange(() => updateActionLayout())); - } - - private setRecommendationText(extension: IExtension, template: IExtensionEditorTemplate): void { - const updateRecommendationText = (layout: boolean) => { - const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason(); - if (extRecommendations[extension.identifier.id.toLowerCase()]) { - const reasonText = extRecommendations[extension.identifier.id.toLowerCase()].reasonText; - if (reasonText) { - append(template.recommendation, $(`div${ThemeIcon.asCSSSelector(starEmptyIcon)}`)); - append(template.recommendation, $(`div.recommendation-text`, undefined, reasonText)); - } - } else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(extension.identifier.id.toLowerCase()) !== -1) { - append(template.recommendation, $(`div.recommendation-text`, undefined, localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension."))); - } - if (layout && this.dimension) { - this.layout(this.dimension); - } - }; - reset(template.recommendation); - if (extension.deprecationInfo || extension.state === ExtensionState.Installed) { - return; - } - updateRecommendationText(false); - this.transientDisposables.add(this.extensionRecommendationsService.onDidChangeRecommendations(() => updateRecommendationText(true))); - } - - private renderMarkdownText(markdownText: string, parent: HTMLElement): IDisposable { - const disposables = new DisposableStore(); - const rendered = disposables.add(renderMarkdown(new MarkdownString(markdownText, { isTrusted: true, supportThemeIcons: true }), { - actionHandler: { - callback: (content) => { - this.openerService.open(content, { allowCommands: true }).catch(onUnexpectedError); - }, - disposables: disposables - } - })); - append(parent, rendered.element); - return disposables; - } - override clearInput(): void { this.contentDisposables.clear(); this.transientDisposables.clear(); @@ -941,7 +874,7 @@ export class ExtensionEditor extends EditorPane { this.contentDisposables.add(scrollableContent); this.renderCategories(content, extension); - this.renderResources(content, extension); + this.renderExtensionResources(content, extension); this.renderMoreInfo(content, extension); append(container, scrollableContent.getDomNode()); @@ -950,11 +883,11 @@ export class ExtensionEditor extends EditorPane { private renderCategories(container: HTMLElement, extension: IExtension): void { if (extension.categories.length) { - const categoriesContainer = append(container, $('.categories-container')); + const categoriesContainer = append(container, $('.categories-container.additional-details-element')); append(categoriesContainer, $('.additional-details-title', undefined, localize('categories', "Categories"))); const categoriesElement = append(categoriesContainer, $('.categories')); for (const category of extension.categories) { - this.transientDisposables.add(this.onClick(append(categoriesElement, $('span.category', undefined, category)), () => { + this.transientDisposables.add(onClick(append(categoriesElement, $('span.category', { tabindex: '0' }, category)), () => { this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) .then(viewlet => viewlet.search(`@category:"${category}"`)); @@ -963,7 +896,7 @@ export class ExtensionEditor extends EditorPane { } } - private renderResources(container: HTMLElement, extension: IExtension): void { + private renderExtensionResources(container: HTMLElement, extension: IExtension): void { const resources: [string, URI][] = []; if (extension.url) { resources.push([localize('Marketplace', "Marketplace"), URI.parse(extension.url)]); @@ -974,34 +907,33 @@ export class ExtensionEditor extends EditorPane { if (extension.url && extension.licenseUrl) { resources.push([localize('license', "License"), URI.parse(extension.licenseUrl)]); } - if (extension.publisherDomain?.verified) { - const publisherDomainUri = URI.parse(extension.publisherDomain.link); - resources.push([publisherDomainUri.authority, publisherDomainUri]); + if (extension.publisherUrl) { + resources.push([extension.publisherDisplayName, extension.publisherUrl]); } - if (resources.length) { - const resourcesContainer = append(container, $('.resources-container')); - append(resourcesContainer, $('.additional-details-title', undefined, localize('resources', "Resources"))); - const resourcesElement = append(resourcesContainer, $('.resources')); + if (resources.length || extension.publisherSponsorLink) { + const extensionResourcesContainer = append(container, $('.resources-container.additional-details-element')); + append(extensionResourcesContainer, $('.additional-details-title', undefined, localize('resources', "Extension Resources"))); + const resourcesElement = append(extensionResourcesContainer, $('.resources')); for (const [label, uri] of resources) { - this.transientDisposables.add(this.onClick(append(resourcesElement, $('a.resource', { title: uri.toString() }, label)), () => this.openerService.open(uri))); + this.transientDisposables.add(onClick(append(resourcesElement, $('a.resource', { title: uri.toString(), tabindex: '0' }, label)), () => this.openerService.open(uri))); } } } private renderMoreInfo(container: HTMLElement, extension: IExtension): void { const gallery = extension.gallery; - const moreInfoContainer = append(container, $('.more-info-container')); - append(moreInfoContainer, $('.additional-details-title', undefined, localize('Marketplace Info', "Marketplace Info"))); + const moreInfoContainer = append(container, $('.more-info-container.additional-details-element')); + append(moreInfoContainer, $('.additional-details-title', undefined, localize('Marketplace Info', "More Info"))); const moreInfo = append(moreInfoContainer, $('.more-info')); if (gallery) { append(moreInfo, $('.more-info-entry', undefined, $('div', undefined, localize('release date', "Released on")), - $('div', undefined, new Date(gallery.releaseDate).toLocaleString(undefined, { hourCycle: 'h23' })) + $('div', undefined, new Date(gallery.releaseDate).toLocaleString(locale, { hourCycle: 'h23' })) ), $('.more-info-entry', undefined, $('div', undefined, localize('last updated', "Last updated")), - $('div', undefined, new Date(gallery.lastUpdated).toLocaleString(undefined, { hourCycle: 'h23' })) + $('div', undefined, new Date(gallery.lastUpdated).toLocaleString(locale, { hourCycle: 'h23' })) ) ); } @@ -1285,7 +1217,7 @@ export class ExtensionEditor extends EditorPane { const contrib = manifest.contributes?.viewsContainers || {}; const viewContainers = Object.keys(contrib).reduce((result, location) => { - let viewContainersForLocation: IViewContainer[] = contrib[location]; + const viewContainersForLocation: IViewContainer[] = contrib[location]; result.push(...viewContainersForLocation.map(viewContainer => ({ ...viewContainer, location }))); return result; }, [] as Array<{ id: string; title: string; location: string }>); @@ -1310,7 +1242,7 @@ export class ExtensionEditor extends EditorPane { const contrib = manifest.contributes?.views || {}; const views = Object.keys(contrib).reduce((result, location) => { - let viewsForLocation: IView[] = contrib[location]; + const viewsForLocation: IView[] = contrib[location]; result.push(...viewsForLocation.map(view => ({ ...view, location }))); return result; }, [] as Array<{ id: string; name: string; location: string }>); @@ -1483,9 +1415,9 @@ export class ExtensionEditor extends EditorPane { } function colorPreview(colorReference: string): Node[] { - let result: Node[] = []; + const result: Node[] = []; if (colorReference && colorReference[0] === '#') { - let color = Color.fromHex(colorReference); + const color = Color.fromHex(colorReference); if (color) { result.push($('span', { class: 'colorBox', style: 'background-color: ' + Color.Format.CSS.format(color) }, '')); } @@ -1825,9 +1757,7 @@ registerAction2(class StartExtensionEditorFindNextAction extends Action2 { } run(accessor: ServicesAccessor): any { const extensionEditor = getExtensionEditor(accessor); - if (extensionEditor) { - extensionEditor.runFindAction(false); - } + extensionEditor?.runFindAction(false); } }); @@ -1847,9 +1777,7 @@ registerAction2(class StartExtensionEditorFindPreviousAction extends Action2 { } run(accessor: ServicesAccessor): any { const extensionEditor = getExtensionEditor(accessor); - if (extensionEditor) { - extensionEditor.runFindAction(true); - } + extensionEditor?.runFindAction(true); } }); @@ -1857,14 +1785,14 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a { color: ${link}; }`); + collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource { color: ${link}; }`); collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a { color: ${link}; }`); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { - collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a:hover, - .monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a:active { color: ${activeLink}; }`); + collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource:hover, + .monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource:active { color: ${activeLink}; }`); collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a:hover, .monaco-workbench .extension-editor .content .feature-contributions a:active { color: ${activeLink}; }`); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 4e95edf7f9e..15f1a403498 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -27,13 +27,17 @@ import { EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtens import { IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; type ExtensionRecommendationsNotificationClassification = { - userReaction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - extensionId?: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + owner: 'sandy081'; + comment: 'Response information when an extension is recommended'; + userReaction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'User reaction after showing the recommendation prompt. Eg., install, cancel, show, neverShowAgain' }; + extensionId?: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Id of the extension that is recommended' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source from which this recommendation is coming from. Eg., file, exe.,' }; }; type ExtensionWorkspaceRecommendationsNotificationClassification = { - userReaction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + owner: 'sandy081'; + comment: 'Response information when a recommendation from workspace is recommended'; + userReaction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'User reaction after showing the recommendation prompt. Eg., install, cancel, show, neverShowAgain' }; }; const ignoreImportantExtensionRecommendationStorageKey = 'extensionsAssistant/importantRecommendationsIgnore'; @@ -113,7 +117,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec // Ignored Important Recommendations get ignoredRecommendations(): string[] { - return distinct([...(<string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendationStorageKey, StorageScope.GLOBAL, '[]')))].map(i => i.toLowerCase())); + return distinct([...(<string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendationStorageKey, StorageScope.PROFILE, '[]')))].map(i => i.toLowerCase())); } private recommendedExtensions: string[] = []; @@ -410,7 +414,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec const importantRecommendationsIgnoreList = [...this.ignoredRecommendations]; if (!importantRecommendationsIgnoreList.includes(id.toLowerCase())) { importantRecommendationsIgnoreList.push(id.toLowerCase()); - this.storageService.store(ignoreImportantExtensionRecommendationStorageKey, JSON.stringify(importantRecommendationsIgnoreList), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(ignoreImportantExtensionRecommendationStorageKey, JSON.stringify(importantRecommendationsIgnoreList), StorageScope.PROFILE, StorageTarget.USER); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 26e23dad630..fcf10ab0122 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -29,8 +29,10 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; type IgnoreRecommendationClassification = { - recommendationReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + owner: 'sandy081'; + comment: 'Report when a recommendation is ignored'; + recommendationReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Reason why extension is recommended' }; + extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Id of the extension recommendation that is being ignored' }; }; export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService { @@ -233,6 +235,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte if (recommendationReason) { /* __GDPR__ "extensionGallery:install:recommendations" : { + "owner": "sandy081", "recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "${include}": [ "${GalleryExtensionTelemetryData}" diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index ebbd930216f..f64e267746c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -75,7 +75,8 @@ 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'; +import { ExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -90,7 +91,7 @@ Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess).registerQuickAccessPro ctor: ManageExtensionsQuickAccessProvider, prefix: ManageExtensionsQuickAccessProvider.PREFIX, placeholder: localize('manageExtensionsQuickAccessPlaceholder', "Press Enter to manage extensions."), - helpEntries: [{ description: localize('manageExtensionsHelp', "Manage Extensions"), needsEditor: false }] + helpEntries: [{ description: localize('manageExtensionsHelp', "Manage Extensions") }] }); // Editor @@ -224,6 +225,11 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration) } }] }, + 'extensions.experimental.useUtilityProcess': { + type: 'boolean', + description: localize('extensionsUseUtilityProcess', "When enabled, the extension host will be launched using the new UtilityProcess Electron API."), + default: false + }, [WORKSPACE_TRUST_EXTENSION_SUPPORT]: { type: 'object', scope: ConfigurationScope.APPLICATION, @@ -491,7 +497,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi ctor: InstallExtensionQuickAccessProvider, prefix: InstallExtensionQuickAccessProvider.PREFIX, placeholder: localize('installExtensionQuickAccessPlaceholder', "Type the name of an extension to install or search."), - helpEntries: [{ description: localize('installExtensionQuickAccessHelp', "Install or Search Extensions"), needsEditor: false }] + helpEntries: [{ description: localize('installExtensionQuickAccessHelp', "Install or Search Extensions") }] }); } } @@ -1336,7 +1342,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }, run: async (accessor: ServicesAccessor, extensionId: string) => { const clipboardService = accessor.get(IClipboardService); - let extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; if (extension) { const name = localize('extensionInfoName', 'Name: {0}', extension.displayName); @@ -1554,6 +1560,16 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi } +class ExtensionStorageCleaner implements IWorkbenchContribution { + + constructor( + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IStorageService storageService: IStorageService, + ) { + ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService); + } +} + const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Restored); @@ -1566,7 +1582,7 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionEnablementWorkspaceTrus workbenchRegistry.registerWorkbenchContribution(ExtensionsCompletionItemsProvider, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(UnsupportedExtensionsMigrationContrib, LifecyclePhase.Eventually); if (isWeb) { - workbenchRegistry.registerWorkbenchContribution(ExtensionsCleaner, LifecyclePhase.Eventually); + workbenchRegistry.registerWorkbenchContribution(ExtensionStorageCleaner, LifecyclePhase.Eventually); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index e578b8359cb..710e811a4f7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -65,6 +65,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { ViewContainerLocation } from 'vs/workbench/common/views'; import { flatten } from 'vs/base/common/arrays'; import { fromNow } from 'vs/base/common/date'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; export class PromptExtensionInstallFailureAction extends Action { @@ -143,7 +144,7 @@ export class PromptExtensionInstallFailureAction extends Action { }); } - let message = `${operationMessage}${additionalMessage ? ` ${additionalMessage}` : ''}`; + const message = `${operationMessage}${additionalMessage ? ` ${additionalMessage}` : ''}`; this.notificationService.prompt(Severity.Error, message, promptChoices); } } @@ -250,6 +251,7 @@ export abstract class AbstractInstallAction extends ExtensionAction { @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @ILabelService private readonly labelService: ILabelService, @IDialogService private readonly dialogService: IDialogService, + @IPreferencesService private readonly preferencesService: IPreferencesService, ) { super(id, localize('install', "Install"), cssClass, false); this.update(); @@ -276,13 +278,35 @@ export abstract class AbstractInstallAction extends ExtensionAction { } if (this.extension.deprecationInfo) { - const result = await this.dialogService.confirm({ - type: 'warning', - message: localize('install confirmation', "Are you sure you want to install '{0}'?", this.extension.displayName), - detail: localize('deprecated message', "This extension is no longer being maintained and is deprecated."), - primaryButton: localize('install anyway', "Install Anyway"), - }); - if (!result.confirmed) { + let detail = localize('deprecated message', "This extension is deprecated as it is no longer being maintained."); + let action: () => Promise<any> = async () => undefined; + const buttons = [ + localize('install anyway', "Install Anyway"), + localize('cancel', "Cancel"), + ]; + + if (this.extension.deprecationInfo.extension) { + detail = localize('deprecated with alternate extension message', "This extension is deprecated. Use the {0} extension instead.", this.extension.deprecationInfo.extension.displayName); + buttons.splice(1, 0, localize('Show alternate extension', "Open {0}", this.extension.deprecationInfo.extension.displayName)); + const alternateExtension = this.extension.deprecationInfo.extension; + action = () => this.extensionsWorkbenchService.getExtensions([{ id: alternateExtension.id, preRelease: alternateExtension.preRelease }], CancellationToken.None) + .then(([extension]) => this.extensionsWorkbenchService.open(extension)); + } else if (this.extension.deprecationInfo.settings) { + detail = localize('deprecated with alternate settings message', "This extension is deprecated as this functionality is now built-in to VS Code."); + buttons.splice(1, 0, localize('configure in settings', "Configure Settings")); + const settings = this.extension.deprecationInfo.settings; + action = () => this.preferencesService.openSettings({ query: settings.map(setting => `@id:${setting}`).join(' ') }); + } + + const result = await this.dialogService.show( + Severity.Warning, + localize('install confirmation', "Are you sure you want to install '{0}'?", this.extension.displayName), + buttons, + { detail, cancelId: buttons.length - 1 }); + if (result.choice === 1) { + return action(); + } + if (result.choice === 2) { return; } } @@ -387,12 +411,13 @@ export class InstallAction extends AbstractInstallAction { @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, @ILabelService labelService: ILabelService, @IDialogService dialogService: IDialogService, + @IPreferencesService preferencesService: IPreferencesService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IWorkbenchExtensionManagementService private readonly workbenchExtensioManagementService: IWorkbenchExtensionManagementService, @IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService, ) { super(`extensions.install`, installPreReleaseVersion, InstallAction.Class, - extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService, dialogService); + extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService, dialogService, preferencesService); this.updateLabel(); this._register(labelService.onDidChangeFormatters(() => this.updateLabel(), this)); this._register(Event.any(userDataSyncEnablementService.onDidChangeEnablement, @@ -453,11 +478,12 @@ export class InstallAndSyncAction extends AbstractInstallAction { @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, @ILabelService labelService: ILabelService, @IDialogService dialogService: IDialogService, + @IPreferencesService preferencesService: IPreferencesService, @IProductService productService: IProductService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, ) { super('extensions.installAndSync', installPreReleaseVersion, AbstractInstallAction.Class, - extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService, dialogService); + extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService, dialogService, preferencesService); this.tooltip = localize({ key: 'install everywhere tooltip', comment: ['Placeholder is the name of the product. Eg: Visual Studio Code or Visual Studio Code - Insiders'] }, "Install this extension in all your synced {0} instances", productService.nameLong); this._register(Event.any(userDataSyncEnablementService.onDidChangeEnablement, Event.filter(userDataSyncEnablementService.onDidChangeResourceEnablement, e => e[0] === SyncResource.Extensions))(() => this.update())); @@ -757,10 +783,15 @@ export class UpdateAction extends ExtensionAction { } private async computeAndUpdateEnablement(): Promise<void> { + this.enabled = false; + this.class = UpdateAction.DisabledClass; + this.label = this.getLabel(); + if (!this.extension) { - this.enabled = false; - this.class = UpdateAction.DisabledClass; - this.label = this.getLabel(); + return; + } + + if (this.extension.deprecationInfo) { return; } @@ -800,6 +831,52 @@ export class UpdateAction extends ExtensionAction { } } +export class MigrateDeprecatedExtensionAction extends ExtensionAction { + + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} prominent migrate`; + private static readonly DisabledClass = `${MigrateDeprecatedExtensionAction.EnabledClass} disabled`; + + constructor( + private readonly small: boolean, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super('extensionsAction.migrateDeprecatedExtension', localize('migrateExtension', "Migrate"), MigrateDeprecatedExtensionAction.DisabledClass, false); + this.update(); + } + + update(): void { + this.enabled = false; + this.class = MigrateDeprecatedExtensionAction.DisabledClass; + if (!this.extension?.local) { + return; + } + if (this.extension.state !== ExtensionState.Installed) { + return; + } + if (!this.extension.deprecationInfo?.extension) { + return; + } + const id = this.extension.deprecationInfo.extension.id; + if (this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, { id }))) { + return; + } + this.enabled = true; + this.class = MigrateDeprecatedExtensionAction.EnabledClass; + this.tooltip = localize('migrate to', "Migrate to {0}", this.extension.deprecationInfo.extension.displayName); + this.label = this.small ? localize('migrate', "Migrate") : this.tooltip; + } + + override async run(): Promise<any> { + if (!this.extension?.deprecationInfo?.extension) { + return; + } + const local = this.extension.local; + await this.extensionsWorkbenchService.uninstall(this.extension); + const [extension] = await this.extensionsWorkbenchService.getExtensions([{ id: this.extension.deprecationInfo.extension.id, preRelease: this.extension.deprecationInfo?.extension?.preRelease }], CancellationToken.None); + await this.extensionsWorkbenchService.install(extension, { isMachineScoped: local?.isMachineScoped }); + } +} + export class ExtensionActionWithDropdownActionViewItem extends ActionWithDropdownActionViewItem { constructor( @@ -844,9 +921,7 @@ export abstract class ExtensionDropDownAction extends ExtensionAction { } public override run({ actionGroups, disposeActionsOnHide }: { actionGroups: IAction[][]; disposeActionsOnHide: boolean }): Promise<any> { - if (this._actionViewItem) { - this._actionViewItem.showMenu(actionGroups, disposeActionsOnHide); - } + this._actionViewItem?.showMenu(actionGroups, disposeActionsOnHide); return Promise.resolve(); } } @@ -862,7 +937,7 @@ export class DropDownMenuActionViewItem extends ActionViewItem { public showMenu(menuActionGroups: IAction[][], disposeActionsOnHide: boolean): void { if (this.element) { const actions = this.getActions(menuActionGroups); - let elementPosition = DOM.getDomNodePagePosition(this.element); + const elementPosition = DOM.getDomNodePagePosition(this.element); const anchor = { x: elementPosition.left, y: elementPosition.top + elementPosition.height + 10 }; this.contextMenuService.showContextMenu({ getAnchor: () => anchor, @@ -1138,7 +1213,7 @@ export class InstallAnotherVersionAction extends ExtensionAction { } update(): void { - this.enabled = !!this.extension && !this.extension.isBuiltin && !!this.extension.gallery && !!this.extension.local && !!this.extension.server && this.extension.state === ExtensionState.Installed; + this.enabled = !!this.extension && !this.extension.isBuiltin && !!this.extension.gallery && !!this.extension.local && !!this.extension.server && this.extension.state === ExtensionState.Installed && !this.extension.deprecationInfo; } override async run(): Promise<any> { @@ -2174,12 +2249,12 @@ export class ExtensionStatusAction extends ExtensionAction { if (this.extension.deprecationInfo) { if (this.extension.deprecationInfo.extension) { const link = `[${this.extension.deprecationInfo.extension.displayName}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.deprecationInfo.extension.id]))}`)})`; - this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('deprecated with alternate extension tooltip', "This extension has been deprecated. Use {0} extension instead.", link)) }, true); + this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('deprecated with alternate extension tooltip', "This extension is deprecated. Use the {0} extension instead.", link)) }, true); } else if (this.extension.deprecationInfo.settings) { const link = `[${localize('settings', "settings")}](${URI.parse(`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify([this.extension.deprecationInfo.settings.map(setting => `@id:${setting}`).join(' ')]))}`)})`; - this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('deprecated with alternate settings tooltip', "This extension is deprecated and has become a native feature in VS Code. Configure these {0} instead.", link)) }, true); + this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('deprecated with alternate settings tooltip', "This extension is deprecated as this functionality is now built-in to VS Code. Configure these {0} to use this functionality.", link)) }, true); } else { - this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('deprecated tooltip', "This extension is no longer being maintained and is deprecated.")) }, true); + this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('deprecated tooltip', "This extension is deprecated as it is no longer being maintained.")) }, true); } return; } @@ -2768,72 +2843,64 @@ export const extensionButtonProminentHoverBackground = registerColor('extensionB registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const foregroundColor = theme.getColor(foreground); if (foregroundColor) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.built-in-status { border-color: ${foregroundColor}; }`); } const buttonBackgroundColor = theme.getColor(buttonBackground); if (buttonBackgroundColor) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label { background-color: ${buttonBackgroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.label { background-color: ${buttonBackgroundColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.label { background-color: ${buttonBackgroundColor}; }`); } const buttonForegroundColor = theme.getColor(buttonForeground); if (buttonForegroundColor) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label { color: ${buttonForegroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.label { color: ${buttonForegroundColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.label { color: ${buttonForegroundColor}; }`); } const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground); if (buttonHoverBackgroundColor) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item:hover .action-label.extension-action.label { background-color: ${buttonHoverBackgroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item:hover .action-label.extension-action.label { background-color: ${buttonHoverBackgroundColor}; }`); + collector.addRule(`.monaco-action-bar .action-item:hover .action-label.extension-action.label { background-color: ${buttonHoverBackgroundColor}; }`); } const extensionButtonProminentBackgroundColor = theme.getColor(extensionButtonProminentBackground); if (extensionButtonProminentBackground) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentBackgroundColor}; }`); } const extensionButtonProminentForegroundColor = theme.getColor(extensionButtonProminentForeground); if (extensionButtonProminentForeground) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label.prominent { color: ${extensionButtonProminentForegroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.label.prominent { color: ${extensionButtonProminentForegroundColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.label.prominent { color: ${extensionButtonProminentForegroundColor}; }`); } const extensionButtonProminentHoverBackgroundColor = theme.getColor(extensionButtonProminentHoverBackground); if (extensionButtonProminentHoverBackground) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item:hover .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item:hover .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); + collector.addRule(`.monaco-action-bar .action-item:hover .action-label.extension-action.label.prominent { background-color: ${extensionButtonProminentHoverBackgroundColor}; }`); } const contrastBorderColor = theme.getColor(contrastBorder); if (contrastBorderColor) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action:not(.disabled) { border: 1px solid ${contrastBorderColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action:not(.disabled) { border: 1px solid ${contrastBorderColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action:not(.disabled) { border: 1px solid ${contrastBorderColor}; }`); } const errorColor = theme.getColor(editorErrorForeground); if (errorColor) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.extension-status-error { color: ${errorColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.extension-status-error { color: ${errorColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.extension-status-error { color: ${errorColor}; }`); + collector.addRule(`.extension-editor .header .actions-status-container > .status ${ThemeIcon.asCSSSelector(errorIcon)} { color: ${errorColor}; }`); collector.addRule(`.extension-editor .body .subcontent .runtime-status ${ThemeIcon.asCSSSelector(errorIcon)} { color: ${errorColor}; }`); collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(errorIcon)} { color: ${errorColor}; }`); } const warningColor = theme.getColor(editorWarningForeground); if (warningColor) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.extension-status-warning { color: ${warningColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.extension-status-warning { color: ${warningColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.extension-status-warning { color: ${warningColor}; }`); + collector.addRule(`.extension-editor .header .actions-status-container > .status ${ThemeIcon.asCSSSelector(warningIcon)} { color: ${warningColor}; }`); collector.addRule(`.extension-editor .body .subcontent .runtime-status ${ThemeIcon.asCSSSelector(warningIcon)} { color: ${warningColor}; }`); collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(warningIcon)} { color: ${warningColor}; }`); } const infoColor = theme.getColor(editorInfoForeground); if (infoColor) { - collector.addRule(`.extension-list-item .monaco-action-bar .action-item .action-label.extension-action.extension-status-info { color: ${infoColor}; }`); - collector.addRule(`.extension-editor .monaco-action-bar .action-item .action-label.extension-action.extension-status-info { color: ${infoColor}; }`); + collector.addRule(`.monaco-action-bar .action-item .action-label.extension-action.extension-status-info { color: ${infoColor}; }`); + collector.addRule(`.extension-editor .header .actions-status-container > .status ${ThemeIcon.asCSSSelector(infoIcon)} { color: ${infoColor}; }`); collector.addRule(`.extension-editor .body .subcontent .runtime-status ${ThemeIcon.asCSSSelector(infoIcon)} { color: ${infoColor}; }`); collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(infoIcon)} { color: ${infoColor}; }`); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts b/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts deleted file mode 100644 index 2d9b7872491..00000000000 --- a/src/vs/workbench/contrib/extensions/browser/extensionsCleaner.ts +++ /dev/null @@ -1,19 +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 { 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/extensions/browser/extensionsIcons.ts b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts index 85e500f4c07..7cf7dc9f362 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts @@ -26,6 +26,7 @@ export const installCountIcon = registerIcon('extensions-install-count', Codicon export const ratingIcon = registerIcon('extensions-rating', Codicon.star, localize('ratingIcon', 'Icon shown along with the rating in the extensions view and editor.')); export const verifiedPublisherIcon = registerIcon('extensions-verified-publisher', Codicon.verifiedFilled, localize('verifiedPublisher', 'Icon used for the verified extension publisher in the extensions view and editor.')); export const preReleaseIcon = registerIcon('extensions-pre-release', Codicon.versions, localize('preReleaseIcon', 'Icon shown for extensions having pre-release versions in extensions view and editor.')); +export const sponsorIcon = registerIcon('extensions-sponsor', Codicon.heartFilled, localize('sponsorIcon', 'Icon used for sponsoring extensions in the extensions view and editor.')); export const starFullIcon = registerIcon('extensions-star-full', Codicon.starFull, localize('starFullIcon', 'Full star icon used for the rating in the extensions editor.')); export const starHalfIcon = registerIcon('extensions-star-half', Codicon.starHalf, localize('starHalfIcon', 'Half star icon used for the rating in the extensions editor.')); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 9b5c1397ee4..1754e2233ab 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -13,7 +13,7 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { UpdateAction, ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { UpdateAction, ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction, SwitchToPreReleaseVersionAction, SwitchToReleasedVersionAction, MigrateDeprecatedExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, extensionVerifiedPublisherIconColor } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; @@ -118,6 +118,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> { const reloadAction = this.instantiationService.createInstance(ReloadAction); const actions = [ this.instantiationService.createInstance(ExtensionStatusLabelAction), + this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, true), this.instantiationService.createInstance(UpdateAction), reloadAction, this.instantiationService.createInstance(InstallDropdownAction), @@ -186,15 +187,17 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> { data.extensionDisposables = dispose(data.extensionDisposables); const updateEnablement = async () => { - let isDisabled = false; + let disabled = false; + const deprecated = !!extension.deprecationInfo; if (extension.state === ExtensionState.Uninstalled) { - isDisabled = !!extension.deprecationInfo || !(await this.extensionsWorkbenchService.canInstall(extension)); + disabled = deprecated || !(await this.extensionsWorkbenchService.canInstall(extension)); } else if (extension.local && !isLanguagePackExtension(extension.local.manifest)) { const runningExtensions = await this.extensionService.getExtensions(); const runningExtension = runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier))[0]; - isDisabled = !(runningExtension && extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); + disabled = !(runningExtension && extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); } - data.root.classList.toggle('disabled', isDisabled); + data.element.classList.toggle('deprecated', deprecated); + data.root.classList.toggle('disabled', disabled); }; updateEnablement(); this.extensionService.onDidChangeExtensions(() => updateEnablement(), this, data.extensionDisposables); @@ -211,8 +214,13 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> { data.name.textContent = extension.displayName; data.description.textContent = extension.description; - data.publisherDisplayName.textContent = extension.publisherDisplayName; - data.verifiedPublisherIcon.style.display = extension.publisherDomain?.verified ? 'inherit' : 'none'; + + const updatePublisher = () => { + data.publisherDisplayName.textContent = extension.publisherDisplayName; + data.verifiedPublisherIcon.style.display = extension.publisherDomain?.verified ? 'inherit' : 'none'; + }; + updatePublisher(); + Event.filter(this.extensionsWorkbenchService.onChange, e => !!e && areSameExtensions(e.identifier, extension.identifier))(() => updatePublisher(), this, data.extensionDisposables); data.installCount.style.display = ''; data.ratings.style.display = ''; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 0d895be2eae..6faf5d3c10a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -21,7 +21,7 @@ import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAct import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, UntrustedWorkspaceUnsupportedExtensionsView, UntrustedWorkspacePartiallySupportedExtensionsView, VirtualWorkspaceUnsupportedExtensionsView, VirtualWorkspacePartiallySupportedExtensionsView, DefaultPopularExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; +import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, BuiltInFeatureExtensionsView, BuiltInThemesExtensionsView, BuiltInProgrammingLanguageExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, UntrustedWorkspaceUnsupportedExtensionsView, UntrustedWorkspacePartiallySupportedExtensionsView, VirtualWorkspaceUnsupportedExtensionsView, VirtualWorkspacePartiallySupportedExtensionsView, DefaultPopularExtensionsView, DeprecatedExtensionsView, SearchMarketplaceExtensionsView } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import Severity from 'vs/base/common/severity'; @@ -58,7 +58,7 @@ import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/act import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { coalesce } from 'vs/base/common/arrays'; -import { extractEditorsDropData } from 'vs/platform/dnd/browser/dnd'; +import { extractEditorsAndFilesDropData } from 'vs/platform/dnd/browser/dnd'; import { extname } from 'vs/base/common/resources'; const SearchMarketplaceExtensionsContext = new RawContextKey<boolean>('searchMarketplaceExtensions', false); @@ -70,6 +70,7 @@ const HasInstalledExtensionsContext = new RawContextKey<boolean>('hasInstalledEx const BuiltInExtensionsContext = new RawContextKey<boolean>('builtInExtensions', false); const SearchBuiltInExtensionsContext = new RawContextKey<boolean>('searchBuiltInExtensions', false); const SearchUnsupportedWorkspaceExtensionsContext = new RawContextKey<boolean>('searchUnsupportedWorkspaceExtensions', false); +const SearchDeprecatedExtensionsContext = new RawContextKey<boolean>('searchDeprecatedExtensions', false); const RecommendedExtensionsContext = new RawContextKey<boolean>('recommendedExtensions', false); export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { @@ -104,6 +105,9 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio /* Trust Required extensions views */ viewDescriptors.push(...this.createUnsupportedWorkspaceExtensionsViewDescriptors()); + /* Other Local Filtered extensions views */ + viewDescriptors.push(...this.createOtherLocalFilteredExtensionsViewDescriptors()); + Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).registerViews(viewDescriptors, this.container); } @@ -264,7 +268,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio viewDescriptors.push({ id: 'workbench.views.extensions.marketplace', name: localize('marketPlace', "Marketplace"), - ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]), + ctorDescriptor: new SyncDescriptor(SearchMarketplaceExtensionsView, [{}]), when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions')), }); @@ -414,6 +418,19 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return viewDescriptors; } + private createOtherLocalFilteredExtensionsViewDescriptors(): IViewDescriptor[] { + const viewDescriptors: IViewDescriptor[] = []; + + viewDescriptors.push({ + id: 'workbench.views.extensions.deprecatedExtensions', + name: localize('deprecated', "Deprecated"), + ctorDescriptor: new SyncDescriptor(DeprecatedExtensionsView, [{}]), + when: ContextKeyExpr.and(SearchDeprecatedExtensionsContext), + }); + + return viewDescriptors; + } + } export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IExtensionsViewPaneContainer { @@ -429,6 +446,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private builtInExtensionsContextKey: IContextKey<boolean>; private searchBuiltInExtensionsContextKey: IContextKey<boolean>; private searchWorkspaceUnsupportedExtensionsContextKey: IContextKey<boolean>; + private searchDeprecatedExtensionsContextKey: IContextKey<boolean>; private recommendedExtensionsContextKey: IContextKey<boolean>; private searchDelayer: Delayer<void>; @@ -465,6 +483,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchIntalledExtensionsContext.bindTo(contextKeyService); this.searchWorkspaceUnsupportedExtensionsContextKey = SearchUnsupportedWorkspaceExtensionsContext.bindTo(contextKeyService); + this.searchDeprecatedExtensionsContextKey = SearchDeprecatedExtensionsContext.bindTo(contextKeyService); this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService); this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService); this.searchDisabledExtensionsContextKey = SearchDisabledExtensionsContext.bindTo(contextKeyService); @@ -473,6 +492,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService); this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); this._register(this.paneCompositeService.onDidPaneCompositeOpen(e => { if (e.viewContainerLocation === ViewContainerLocation.Sidebar) { this.onViewletOpen(e.composite); } }, this)); + this._register(extensionsWorkbenchService.onReset(() => this.refresh())); this.searchViewletState = this.getMemento(StorageScope.WORKSPACE, StorageTarget.USER); } @@ -541,7 +561,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE if (this.isSupportedDragElement(e)) { hide(overlay); - const vsixs = coalesce((await this.instantiationService.invokeFunction(accessor => extractEditorsDropData(accessor, e))) + const vsixs = coalesce((await this.instantiationService.invokeFunction(accessor => extractEditorsAndFilesDropData(accessor, e))) .map(editor => editor.resource && extname(editor.resource) === '.vsix' ? editor.resource : undefined)); if (vsixs.length > 0) { @@ -571,9 +591,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.root.classList.toggle('narrow', dimension.width <= 250); this.root.classList.toggle('mini', dimension.width <= 200); } - if (this.searchBox) { - this.searchBox.layout(new Dimension(dimension.width - 34 - /*padding*/8, 20)); - } + this.searchBox?.layout(new Dimension(dimension.width - 34 - /*padding*/8, 20)); super.layout(new Dimension(dimension.width, dimension.height - 41)); } @@ -607,6 +625,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private normalizedQuery(): string { return this.searchBox ? this.searchBox.getValue() + .trim() .replace(/@category/g, 'category') .replace(/@tag:/g, 'tag:') .replace(/@ext:/g, 'ext:') @@ -635,6 +654,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value)); this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isSearchBuiltInExtensionsQuery(value)); this.searchWorkspaceUnsupportedExtensionsContextKey.set(ExtensionsListView.isSearchWorkspaceUnsupportedExtensionsQuery(value)); + this.searchDeprecatedExtensionsContextKey.set(ExtensionsListView.isSearchDeprecatedExtensionsQuery(value)); this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value)); this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery); this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index de628c54a8d..1ed384b4e22 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -39,7 +39,7 @@ import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IAction, Action, Separator, ActionRunner } from 'vs/base/common/actions'; import { ExtensionIdentifier, ExtensionUntrustedWorkspaceSupportType, ExtensionVirtualWorkspaceSupportType, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; -import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs/base/common/async'; import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -60,10 +60,6 @@ import { isOfflineError } from 'vs/base/parts/request/common/request'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.git-base', 'vscode.search-result']; -type WorkspaceRecommendationsClassification = { - count: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'isMeasurement': true }; -}; - class ExtensionsViewState extends Disposable implements IExtensionsViewState { private readonly _onFocus: Emitter<IExtension> = this._register(new Emitter<IExtension>()); @@ -142,7 +138,7 @@ export class ExtensionsListView extends ViewPane { super({ ...(viewletViewOptions as IViewPaneOptions), showActionsAlways: true, - maximumBodySize: options.flexibleHeight ? (storageService.getNumber(`${viewletViewOptions.id}.size`, StorageScope.GLOBAL, 0) ? undefined : 0) : undefined + maximumBodySize: options.flexibleHeight ? (storageService.getNumber(`${viewletViewOptions.id}.size`, StorageScope.PROFILE, 0) ? undefined : 0) : undefined }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); if (this.options.onDidChangeTitle) { this._register(this.options.onDidChangeTitle(title => this.updateTitle(title))); @@ -180,7 +176,12 @@ export class ExtensionsListView extends ViewPane { horizontalScrolling: false, accessibilityProvider: <IListAccessibilityProvider<IExtension | null>>{ getAriaLabel(extension: IExtension | null): string { - return extension ? localize('extension.arialabel', "{0}, {1}, {2}, {3}", extension.displayName, extension.version, extension.publisherDisplayName, extension.description) : ''; + if (!extension) { + return ''; + } + const publisher = localize('extension.arialabel.publihser', "Publisher {0}", extension.publisherDisplayName); + const deprecated = extension?.deprecationInfo ? localize('extension.arialabel.deprecated', "Deprecated") : ''; + return `${extension.displayName}, ${deprecated ? `${deprecated}, ` : ''}${extension.version}, ${publisher}, ${extension.description}`; }, getWidgetAriaLabel(): string { return localize('extensions', "Extensions"); @@ -213,9 +214,7 @@ export class ExtensionsListView extends ViewPane { if (this.bodyTemplate) { this.bodyTemplate.extensionsList.style.height = height + 'px'; } - if (this.list) { - this.list.layout(height, width); - } + this.list?.layout(height, width); } async show(query: string, refresh?: boolean): Promise<IPagedModel<IExtension>> { @@ -234,7 +233,7 @@ export class ExtensionsListView extends ViewPane { const parsedQuery = Query.parse(query); - let options: IQueryOptions = { + const options: IQueryOptions = { sortOrder: SortOrder.Default }; @@ -351,7 +350,7 @@ export class ExtensionsListView extends ViewPane { private async queryLocal(query: Query, options: IQueryOptions): Promise<IQueryResult> { const local = await this.extensionsWorkbenchService.queryLocal(this.options.server); const runningExtensions = await this.extensionService.getExtensions(); - let { extensions, canIncludeInstalledExtensions } = this.filterLocal(local, runningExtensions, query, options); + let { extensions, canIncludeInstalledExtensions } = await this.filterLocal(local, runningExtensions, query, options); const disposables = new DisposableStore(); const onDidChangeModel = disposables.add(new Emitter<IPagedModel<IExtension>>()); @@ -364,7 +363,7 @@ export class ExtensionsListView extends ViewPane { ), () => undefined)(async () => { const local = this.options.server ? this.extensionsWorkbenchService.installed.filter(e => e.server === this.options.server) : this.extensionsWorkbenchService.local; const runningExtensions = await this.extensionService.getExtensions(); - const { extensions: newExtensions } = this.filterLocal(local, runningExtensions, query, options); + const { extensions: newExtensions } = await this.filterLocal(local, runningExtensions, query, options); if (!isDisposed) { const mergedExtensions = this.mergeAddedExtensions(extensions, newExtensions); if (mergedExtensions) { @@ -382,8 +381,8 @@ export class ExtensionsListView extends ViewPane { }; } - private filterLocal(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): { extensions: IExtension[]; canIncludeInstalledExtensions: boolean } { - let value = query.value; + private async filterLocal(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): Promise<{ extensions: IExtension[]; canIncludeInstalledExtensions: boolean }> { + const value = query.value; let extensions: IExtension[] = []; let canIncludeInstalledExtensions = true; @@ -412,6 +411,10 @@ export class ExtensionsListView extends ViewPane { extensions = this.filterWorkspaceUnsupportedExtensions(local, query, options); } + else if (/@deprecated/i.test(query.value)) { + extensions = await this.filterDeprecatedExtensions(local, query, options); + } + return { extensions, canIncludeInstalledExtensions }; } @@ -432,7 +435,7 @@ export class ExtensionsListView extends ViewPane { value = value.replace(/@builtin/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); - let result = local + const result = local .filter(e => e.isBuiltin && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)); const isThemeExtension = (e: IExtension): boolean => { @@ -565,7 +568,7 @@ export class ExtensionsListView extends ViewPane { private filterWorkspaceUnsupportedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] { // shows local extensions which are restricted or disabled in the current workspace because of the extension's capability - let queryString = query.value; // @sortby is already filtered out + const queryString = query.value; // @sortby is already filtered out const match = queryString.match(/^\s*@workspaceUnsupported(?::(untrusted|virtual)(Partial)?)?(?:\s+([^\s]*))?/i); if (!match) { @@ -622,6 +625,13 @@ export class ExtensionsListView extends ViewPane { return this.sortExtensions(local, options); } + private async filterDeprecatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): Promise<IExtension[]> { + const value = query.value.replace(/@deprecated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); + const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest(); + const deprecatedExtensionIds = Object.keys(extensionsControlManifest.deprecated); + local = local.filter(e => deprecatedExtensionIds.includes(e.identifier.id) && (!value || e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)); + return this.sortExtensions(local, options); + } private mergeAddedExtensions(extensions: IExtension[], newExtensions: IExtension[]): IExtension[] | undefined { const oldExtensions = [...extensions]; @@ -831,7 +841,6 @@ export class ExtensionsListView extends ViewPane { private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> { const recommendations = await this.getWorkspaceRecommendations(); const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)); - this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length }); const result: IExtension[] = coalesce(recommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); return new PagedModel(result); } @@ -948,7 +957,7 @@ export class ExtensionsListView extends ViewPane { private updateSize() { if (this.options.flexibleHeight) { this.maximumBodySize = this.list?.model.length ? Number.POSITIVE_INFINITY : 0; - this.storageService.store(`${this.id}.size`, this.list?.model.length || 0, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(`${this.id}.size`, this.list?.model.length || 0, StorageScope.PROFILE, StorageTarget.MACHINE); } } @@ -1017,6 +1026,7 @@ export class ExtensionsListView extends ViewPane { || this.isBuiltInExtensionsQuery(query) || this.isSearchBuiltInExtensionsQuery(query) || this.isBuiltInGroupExtensionsQuery(query) + || this.isSearchDeprecatedExtensionsQuery(query) || this.isSearchWorkspaceUnsupportedExtensionsQuery(query); } @@ -1052,6 +1062,10 @@ export class ExtensionsListView extends ViewPane { return /@disabled/i.test(query); } + static isSearchDeprecatedExtensionsQuery(query: string): boolean { + return /@deprecated\s?.*/i.test(query); + } + static isRecommendedExtensionsQuery(query: string): boolean { return /^@recommended$/i.test(query.trim()); } @@ -1187,6 +1201,30 @@ export class VirtualWorkspacePartiallySupportedExtensionsView extends Extensions } } +export class DeprecatedExtensionsView extends ExtensionsListView { + override async show(query: string): Promise<IPagedModel<IExtension>> { + return ExtensionsListView.isSearchDeprecatedExtensionsQuery(query) ? super.show(query) : this.showEmptyModel(); + } +} + +export class SearchMarketplaceExtensionsView extends ExtensionsListView { + + private readonly reportSearchFinishedDelayer = this._register(new ThrottledDelayer(2000)); + private searchWaitPromise: Promise<void> = Promise.resolve(); + + override async show(query: string): Promise<IPagedModel<IExtension>> { + const queryPromise = super.show(query); + this.reportSearchFinishedDelayer.trigger(() => this.reportSearchFinished()); + this.searchWaitPromise = queryPromise.then(null, null); + return queryPromise; + } + + private async reportSearchFinished(): Promise<void> { + await this.searchWaitPromise; + this.telemetryService.publicLog2('extensionsView:MarketplaceSearchFinished'); + } +} + export class DefaultRecommendedExtensionsView extends ExtensionsListView { private readonly recommendedExtensionsQuery = '@recommended:all'; @@ -1239,8 +1277,8 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView imple } override async show(query: string): Promise<IPagedModel<IExtension>> { - let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace'; - let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery)); + const shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace'; + const model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery)); this.setExpanded(model.length > 0); return model; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 1760db32044..0271565b5fd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -7,21 +7,21 @@ import 'vs/css!./media/extensionsWidgets'; import * as semver from 'vs/base/common/semver/semver'; import { Disposable, toDisposable, DisposableStore, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, ExtensionState, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions'; -import { append, $ } from 'vs/base/browser/dom'; +import { append, $, reset, addDisposableListener, EventType, finalHandler } from 'vs/base/browser/dom'; import * as platform from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { EnablementState, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; +import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { ILabelService } from 'vs/platform/label/common/label'; import { extensionButtonProminentBackground, extensionButtonProminentForeground, ExtensionStatusAction, ReloadAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IThemeService, ThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { EXTENSION_BADGE_REMOTE_BACKGROUND, EXTENSION_BADGE_REMOTE_FOREGROUND } from 'vs/workbench/common/theme'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; -import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, verifiedPublisherIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; +import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, verifiedPublisherIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { registerColor, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; @@ -32,6 +32,13 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import Severity from 'vs/base/common/severity'; import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; import { Color } from 'vs/base/common/color'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -41,6 +48,20 @@ export abstract class ExtensionWidget extends Disposable implements IExtensionCo abstract render(): void; } +export function onClick(element: HTMLElement, callback: () => void): IDisposable { + const disposables: DisposableStore = new DisposableStore(); + disposables.add(addDisposableListener(element, EventType.CLICK, finalHandler(callback))); + disposables.add(addDisposableListener(element, EventType.KEY_UP, e => { + const keyboardEvent = new StandardKeyboardEvent(e); + if (keyboardEvent.equals(KeyCode.Space) || keyboardEvent.equals(KeyCode.Enter)) { + e.preventDefault(); + e.stopPropagation(); + callback(); + } + })); + return disposables; +} + export class InstallCountWidget extends ExtensionWidget { constructor( @@ -160,6 +181,46 @@ export class RatingsWidget extends ExtensionWidget { } } +export class SponsorWidget extends ExtensionWidget { + + private disposables = this._register(new DisposableStore()); + + constructor( + private container: HTMLElement, + @IOpenerService private readonly openerService: IOpenerService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + ) { + super(); + this.render(); + } + + render(): void { + reset(this.container); + this.disposables.clear(); + if (!this.extension?.publisherSponsorLink) { + return; + } + + const sponsor = append(this.container, $('span.sponsor.clickable', { tabIndex: 0, title: this.extension?.publisherSponsorLink })); + sponsor.setAttribute('role', 'link'); // #132645 + const sponsorIconElement = renderIcon(sponsorIcon); + const label = $('span', undefined, localize('sponsor', "Sponsor")); + append(sponsor, sponsorIconElement, label); + this.disposables.add(onClick(sponsor, () => { + type SponsorExtensionClassification = { + owner: 'sandy081'; + comment: 'Reporting when sponosor extension action is executed'; + 'extensionId': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension to be sponsored' }; + }; + type SponsorExtensionEvent = { + 'extensionId': string; + }; + this.telemetryService.publicLog2<SponsorExtensionEvent, SponsorExtensionClassification>('extensionsAction.sponsorExtension', { extensionId: this.extension!.identifier.id }); + this.openerService.open(this.extension!.publisherSponsorLink!); + })); + } +} + export class RecommendationWidget extends ExtensionWidget { private element?: HTMLElement; @@ -224,9 +285,6 @@ export class PreReleaseBookmarkWidget extends ExtensionWidget { if (!this.extension) { return; } - if (this.extension.isBuiltin) { - return; - } if (!this.extension.hasPreReleaseVersion) { return; } @@ -469,6 +527,36 @@ export class ExtensionHoverWidget extends ExtensionWidget { } markdown.appendText(`\n`); + if (this.extension.state === ExtensionState.Installed) { + let addSeparator = false; + const installLabel = InstallCountWidget.getInstallLabel(this.extension, true); + if (installLabel) { + if (addSeparator) { + markdown.appendText(` | `); + } + markdown.appendMarkdown(`$(${installCountIcon.id}) ${installLabel}`); + addSeparator = true; + } + if (this.extension.rating) { + if (addSeparator) { + markdown.appendText(` | `); + } + const rating = Math.round(this.extension.rating * 2) / 2; + markdown.appendMarkdown(`$(${starFullIcon.id}) [${rating}](${this.extension.url}&ssr=false#review-details)`); + addSeparator = true; + } + if (this.extension.publisherSponsorLink) { + if (addSeparator) { + markdown.appendText(` | `); + } + markdown.appendMarkdown(`$(${sponsorIcon.id}) [${localize('sponsor', "Sponsor")}](${this.extension.publisherSponsorLink})`); + addSeparator = true; + } + if (addSeparator) { + markdown.appendText(`\n`); + } + } + if (this.extension.description) { markdown.appendMarkdown(`${this.extension.description}`); markdown.appendText(`\n`); @@ -576,15 +664,103 @@ export class ExtensionHoverWidget extends ExtensionWidget { } -// Rating icon +export class ExtensionStatusWidget extends ExtensionWidget { + + private readonly renderDisposables = this._register(new DisposableStore()); + + private readonly _onDidRender = this._register(new Emitter<void>()); + readonly onDidRender: Event<void> = this._onDidRender.event; + + constructor( + private readonly container: HTMLElement, + private readonly extensionStatusAction: ExtensionStatusAction, + @IOpenerService private readonly openerService: IOpenerService, + ) { + super(); + this.render(); + this._register(extensionStatusAction.onDidChangeStatus(() => this.render())); + } + + render(): void { + reset(this.container); + const extensionStatus = this.extensionStatusAction.status; + if (extensionStatus) { + const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); + if (extensionStatus.icon) { + markdown.appendMarkdown(`$(${extensionStatus.icon.id}) `); + } + markdown.appendMarkdown(extensionStatus.message.value); + const rendered = this.renderDisposables.add(renderMarkdown(markdown, { + actionHandler: { + callback: (content) => { + this.openerService.open(content, { allowCommands: true }).catch(onUnexpectedError); + }, + disposables: this.renderDisposables + } + })); + append(this.container, rendered.element); + } + this._onDidRender.fire(); + } +} + +export class ExtensionRecommendationWidget extends ExtensionWidget { + + private readonly _onDidRender = this._register(new Emitter<void>()); + readonly onDidRender: Event<void> = this._onDidRender.event; + + constructor( + private readonly container: HTMLElement, + @IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService, + @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, + ) { + super(); + this.render(); + this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.render())); + } + + render(): void { + reset(this.container); + const recommendationStatus = this.getRecommendationStatus(); + if (recommendationStatus) { + if (recommendationStatus.icon) { + append(this.container, $(`div${ThemeIcon.asCSSSelector(recommendationStatus.icon)}`)); + } + append(this.container, $(`div.recommendation-text`, undefined, recommendationStatus.message)); + } + this._onDidRender.fire(); + } + + private getRecommendationStatus(): { icon: ThemeIcon | undefined; message: string } | undefined { + if (!this.extension + || this.extension.deprecationInfo + || this.extension.state === ExtensionState.Installed + ) { + return undefined; + } + const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason(); + if (extRecommendations[this.extension.identifier.id.toLowerCase()]) { + const reasonText = extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText; + if (reasonText) { + return { icon: starEmptyIcon, message: reasonText }; + } + } else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(this.extension.identifier.id.toLowerCase()) !== -1) { + return { icon: undefined, message: localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.") }; + } + return undefined; + } +} + export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), true); export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', { dark: textLinkForeground, light: textLinkForeground, hcDark: textLinkForeground, hcLight: textLinkForeground }, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true); export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), true); +export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), true); registerThemingParticipant((theme, collector) => { const extensionRatingIcon = theme.getColor(extensionRatingIconColor); if (extensionRatingIcon) { collector.addRule(`.extension-ratings .codicon-extensions-star-full, .extension-ratings .codicon-extensions-star-half { color: ${extensionRatingIcon}; }`); + collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(starFullIcon)} { color: ${extensionRatingIcon}; }`); } const fgColor = theme.getColor(extensionButtonProminentForeground); @@ -603,4 +779,6 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`${ThemeIcon.asCSSSelector(verifiedPublisherIcon)} { color: ${extensionVerifiedPublisherIcon}; }`); } + collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`); + collector.addRule(`.extension-editor > .header > .details > .subtitle .sponsor ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ece35b058d1..a533f0ad244 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -14,7 +14,7 @@ import { IPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, - InstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, + InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, IExtensionsControlManifest, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -32,9 +32,9 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform, ExtensionIdentifier, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IProductService } from 'vs/platform/product/common/productService'; import { FileAccess } from 'vs/base/common/network'; @@ -57,6 +57,7 @@ interface InstalledExtensionsEvent { readonly count: number; } interface ExtensionsLoadClassification extends GDPRClassification<InstalledExtensionsEvent> { + owner: 'digitarald'; readonly extensionIds: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; readonly count: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; } @@ -124,10 +125,22 @@ export class Extension implements IExtension { return this.local!.manifest.publisher; } + get publisherUrl(): URI | undefined { + if (!this.productService.extensionsGallery || !this.gallery) { + return undefined; + } + + return resources.joinPath(URI.parse(this.productService.extensionsGallery.publisherUrl), this.publisher); + } + get publisherDomain(): { link: string; verified: boolean } | undefined { return this.gallery?.publisherDomain; } + get publisherSponsorLink(): URI | undefined { + return this.gallery?.publisherSponsorLink ? URI.parse(this.gallery.publisherSponsorLink) : undefined; + } + get version(): string { return this.local ? this.local.manifest.version : this.latestVersion; } @@ -403,8 +416,11 @@ class Extensions extends Disposable { extension.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()] : undefined; } - private readonly _onChange: Emitter<{ extension: Extension; operation?: InstallOperation } | undefined> = this._register(new Emitter<{ extension: Extension; operation?: InstallOperation } | undefined>()); - get onChange(): Event<{ extension: Extension; operation?: InstallOperation } | undefined> { return this._onChange.event; } + private readonly _onChange = this._register(new Emitter<{ extension: Extension; operation?: InstallOperation } | undefined>()); + get onChange() { return this._onChange.event; } + + private readonly _onReset = this._register(new Emitter<void>()); + get onReset() { return this._onReset.event; } private installing: Extension[] = []; private uninstalling: Extension[] = []; @@ -420,8 +436,9 @@ class Extensions extends Disposable { super(); this._register(server.extensionManagementService.onInstallExtension(e => this.onInstallExtension(e))); this._register(server.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e))); - this._register(server.extensionManagementService.onUninstallExtension(e => this.onUninstallExtension(e))); + this._register(server.extensionManagementService.onUninstallExtension(e => this.onUninstallExtension(e.identifier))); this._register(server.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e))); + this._register(server.extensionManagementService.onDidChangeProfileExtensions(e => this.onDidChangeProfileExtensions(e.added, e.removed))); this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e))); } @@ -541,7 +558,24 @@ class Extensions extends Disposable { } } - private onDidInstallExtensions(results: readonly InstallExtensionResult[]): void { + private async onDidChangeProfileExtensions(added: ILocalExtension[], removed: ILocalExtension[]): Promise<void> { + const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest(); + for (const addedExtension of added) { + if (this.installed.find(e => areSameExtensions(e.identifier, addedExtension.identifier))) { + const extension = this.instantiationService.createInstance(Extension, this.stateProvider, this.server, addedExtension, undefined); + this.installed.push(extension); + Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); + } + } + + if (removed.length) { + this.installed = this.installed.filter(e => !removed.some(removedExtension => areSameExtensions(e.identifier, removedExtension.identifier))); + } + + this._onReset.fire(); + } + + private async onDidInstallExtensions(results: readonly InstallExtensionResult[]): Promise<void> { for (const event of results) { const { local, source } = event; const gallery = source && !URI.isUri(source) ? source : undefined; @@ -564,12 +598,13 @@ class Extensions extends Disposable { if (!extension.gallery) { extension.gallery = gallery; } + Extensions.updateExtensionFromControlManifest(extension, await this.server.extensionManagementService.getExtensionsControlManifest()); extension.enablementState = this.extensionEnablementService.getEnablementState(local); } } this._onChange.fire(!local || !extension ? undefined : { extension, operation: event.operation }); if (extension && extension.local && !extension.gallery) { - this.syncInstalledExtensionWithGallery(extension); + await this.syncInstalledExtensionWithGallery(extension); } } } @@ -646,6 +681,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private readonly _onChange: Emitter<IExtension | undefined> = new Emitter<IExtension | undefined>(); get onChange(): Event<IExtension | undefined> { return this._onChange.event; } + private readonly _onReset = new Emitter<void>(); + get onReset() { return this._onReset.event; } + readonly preferPreReleases = this.productService.quality !== 'stable'; private installing: IExtension[] = []; @@ -682,14 +720,17 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensionManagementServerService.localExtensionManagementServer) { this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext))); this._register(this.localExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); + this._register(this.localExtensions.onReset(e => { this._onChange.fire(undefined); this._onReset.fire(); })); } if (extensionManagementServerService.remoteExtensionManagementServer) { this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext))); this._register(this.remoteExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); + this._register(this.remoteExtensions.onReset(e => { this._onChange.fire(undefined); this._onReset.fire(); })); } if (extensionManagementServerService.webExtensionManagementServer) { this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext))); this._register(this.webExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); + this._register(this.webExtensions.onReset(e => { this._onChange.fire(undefined); this._onReset.fire(); })); } this.updatesCheckDelayer = new ThrottledDelayer<void>(ExtensionsWorkbenchService.UpdatesCheckInterval); @@ -735,6 +776,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.updateContexts(); this.updateActivity(); })); + + this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorage(e))); } private _reportTelemetry() { const extensionIds = this.installed.filter(extension => @@ -1074,8 +1117,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Skip if check updates only for builtin extensions and current extension is not builtin. continue; } - if (installed.isBuiltin && (!installed.local?.identifier.uuid || this.productService.quality !== 'stable')) { - // Skip if the builtin extension does not have Marketplace identifier or the current quality is not stable. + if (installed.isBuiltin && (!installed.local?.identifier.uuid || (!isWeb && this.productService.quality === 'stable'))) { + // Skip checking updates for a builtin extension if it does not has Marketplace identifier or the current product is VS Code Desktop stable. continue; } infos.push({ ...installed.identifier, preRelease: !!installed.local?.preRelease }); @@ -1187,7 +1230,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return false; } - install(extension: URI | IExtension, installOptions?: InstallOptions | InstallVSIXOptions): Promise<IExtension> { + install(extension: URI | IExtension, installOptions?: InstallOptions | InstallVSIXOptions, progressLocation?: ProgressLocation): Promise<IExtension> { if (extension instanceof URI) { return this.installWithProgress(() => this.installFromVSIX(extension, installOptions)); } @@ -1202,7 +1245,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return Promise.reject(new Error('Missing gallery')); } - return this.installWithProgress(() => this.installFromGallery(extension, gallery, installOptions), gallery.displayName); + return this.installWithProgress(() => this.installFromGallery(extension, gallery, installOptions), gallery.displayName, progressLocation); } setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise<void> { @@ -1233,7 +1276,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension throw new Error('Missing gallery'); } - const [gallery] = await this.galleryService.getExtensions([{ id: extension.gallery.identifier.id, version }], CancellationToken.None); + const targetPlatform = extension.server ? await extension.server.extensionManagementService.getTargetPlatform() : undefined; + const [gallery] = await this.galleryService.getExtensions([{ id: extension.gallery.identifier.id, version }], { targetPlatform }, CancellationToken.None); if (!gallery) { throw new Error(nls.localize('not found', "Unable to install extension '{0}' because the requested version '{1}' is not found.", extension.gallery!.identifier.id, version)); } @@ -1299,10 +1343,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return extension; } - private installWithProgress<T>(installTask: () => Promise<T>, extensionName?: string): Promise<T> { + private installWithProgress<T>(installTask: () => Promise<T>, extensionName?: string, progressLocation?: ProgressLocation): Promise<T> { const title = extensionName ? nls.localize('installing named extension', "Installing '{0}' extension....", extensionName) : nls.localize('installing extension', 'Installing extension....'); return this.progressService.withProgress({ - location: ProgressLocation.Extensions, + location: progressLocation ?? ProgressLocation.Extensions, title }, () => installTask()); } @@ -1316,7 +1360,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.ignoreAutoUpdate(new ExtensionKey(identifier, manifest.version)); } - return this.local.filter(local => areSameExtensions(local.identifier, identifier))[0]; + return this.waitAndGetInstalledExtension(identifier); } private async installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallOptions): Promise<IExtension> { @@ -1328,13 +1372,26 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } else { await this.extensionManagementService.installFromGallery(gallery, installOptions); } - return this.local.filter(local => areSameExtensions(local.identifier, gallery.identifier))[0]; + return this.waitAndGetInstalledExtension(gallery.identifier); } finally { this.installing = this.installing.filter(e => e !== extension); this._onChange.fire(this.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]); } } + private async waitAndGetInstalledExtension(identifier: IExtensionIdentifier): Promise<IExtension> { + let installedExtension = this.local.find(local => areSameExtensions(local.identifier, identifier)); + if (!installedExtension) { + await Event.toPromise(Event.filter(this.onChange, e => !!e && this.local.some(local => areSameExtensions(local.identifier, identifier)))); + } + installedExtension = this.local.find(local => areSameExtensions(local.identifier, identifier)); + if (!installedExtension) { + // This should not happen + throw new Error('Extension should have been installed'); + } + return installedExtension; + } + private promptAndSetEnablement(extensions: IExtension[], enablementState: EnablementState): Promise<any> { const enable = enablementState === EnablementState.EnabledGlobally || enablementState === EnablementState.EnabledWorkspace; if (enable) { @@ -1354,7 +1411,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const enable = enablementState === EnablementState.EnabledGlobally || enablementState === EnablementState.EnabledWorkspace; if (!enable) { for (const extension of extensions) { - let dependents = this.getDependentsAfterDisablement(extension, allExtensions, this.local); + const dependents = this.getDependentsAfterDisablement(extension, allExtensions, this.local); if (dependents.length) { return new Promise<void>((resolve, reject) => { this.notificationService.prompt(Severity.Error, this.getDependentsErrorMessage(extension, allExtensions, dependents), [ @@ -1428,7 +1485,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private getDependentsErrorMessage(extension: IExtension, allDisabledExtensions: IExtension[], dependents: IExtension[]): string { for (const e of [extension, ...allDisabledExtensions]) { - let dependentsOfTheExtension = dependents.filter(d => d.dependencies.some(id => areSameExtensions({ id }, e.identifier))); + const dependentsOfTheExtension = dependents.filter(d => d.dependencies.some(id => areSameExtensions({ id }, e.identifier))); if (dependentsOfTheExtension.length) { return this.getErrorMessageForDisablingAnExtensionWithDependents(e, dependentsOfTheExtension); } @@ -1454,6 +1511,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (changed[i]) { /* __GDPR__ "extension:enable" : { + "owner": "sandy081", "${include}": [ "${GalleryExtensionTelemetryData}" ] @@ -1461,6 +1519,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension */ /* __GDPR__ "extension:disable" : { + "owner": "sandy081", "${include}": [ "${GalleryExtensionTelemetryData}" ] @@ -1489,9 +1548,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => new Promise(resolve => this._activityCallBack = resolve)); } } else { - if (this._activityCallBack) { - this._activityCallBack(); - } + this._activityCallBack?.(); this._activityCallBack = null; } } @@ -1540,18 +1597,26 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }).then(undefined, error => this.onError(error)); } + //#region Ignore Autoupdates when specific versions are installed + /* TODO: @sandy081 Extension version shall be moved to extensions.json file */ private _ignoredAutoUpdateExtensions: string[] | undefined; private get ignoredAutoUpdateExtensions(): string[] { if (!this._ignoredAutoUpdateExtensions) { - this._ignoredAutoUpdateExtensions = JSON.parse(this.storageService.get('extensions.ignoredAutoUpdateExtension', StorageScope.GLOBAL, '[]') || '[]'); + this._ignoredAutoUpdateExtensions = JSON.parse(this.storageService.get('extensions.ignoredAutoUpdateExtension', StorageScope.PROFILE, '[]') || '[]'); } return this._ignoredAutoUpdateExtensions!; } private set ignoredAutoUpdateExtensions(extensionIds: string[]) { this._ignoredAutoUpdateExtensions = distinct(extensionIds.map(id => id.toLowerCase())); - this.storageService.store('extensions.ignoredAutoUpdateExtension', JSON.stringify(this._ignoredAutoUpdateExtensions), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store('extensions.ignoredAutoUpdateExtension', JSON.stringify(this._ignoredAutoUpdateExtensions), StorageScope.PROFILE, StorageTarget.MACHINE); + } + + private onDidChangeStorage(e: IStorageValueChangeEvent): void { + if (e.scope === StorageScope.PROFILE && e.key === 'extensions.ignoredAutoUpdateExtension') { + this._ignoredAutoUpdateExtensions = undefined; + } } private ignoreAutoUpdate(extensionKey: ExtensionKey): void { @@ -1568,4 +1633,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.ignoredAutoUpdateExtensions = this.ignoredAutoUpdateExtensions.filter(extensionId => this.local.some(local => !!local.local && new ExtensionKey(local.identifier, local.version).toString() === extensionId)); } + //#endregion + } diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index b81342b1bb9..d1b91197ff4 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -37,8 +37,10 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; type FileExtensionSuggestionClassification = { - userReaction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - fileExtension: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + owner: 'sandy081'; + comment: 'Response information when a file based reccommendation is suggested'; + userReaction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'User reaction after showing the recommendation prompt. Eg., install, cancel, show, neverShowAgain' }; + fileExtension: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Extension of the file for which an extension is being recommended.' }; }; const promptedRecommendationsStorageKey = 'fileBasedRecommendations/promptedRecommendations'; @@ -208,7 +210,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } return false; }); - let languageName: string | null = importantRecommendations.length ? this.languageService.getLanguageName(language) : null; + const languageName: string | null = importantRecommendations.length ? this.languageService.getLanguageName(language) : null; const fileBasedRecommendations: string[] = [...importantRecommendations]; for (let [pattern, extensionIds] of this.fileBasedRecommendationsByPattern) { @@ -285,23 +287,23 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } private getPromptedRecommendations(): IStringDictionary<string[]> { - return JSON.parse(this.storageService.get(promptedRecommendationsStorageKey, StorageScope.GLOBAL, '{}')); + return JSON.parse(this.storageService.get(promptedRecommendationsStorageKey, StorageScope.PROFILE, '{}')); } private addToPromptedRecommendations(exeName: string, extensions: string[]) { const promptedRecommendations = this.getPromptedRecommendations(); promptedRecommendations[exeName] = extensions; - this.storageService.store(promptedRecommendationsStorageKey, JSON.stringify(promptedRecommendations), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(promptedRecommendationsStorageKey, JSON.stringify(promptedRecommendations), StorageScope.PROFILE, StorageTarget.USER); } private getPromptedFileExtensions(): string[] { - return JSON.parse(this.storageService.get(promptedFileExtensionsStorageKey, StorageScope.GLOBAL, '[]')); + return JSON.parse(this.storageService.get(promptedFileExtensionsStorageKey, StorageScope.PROFILE, '[]')); } private addToPromptedFileExtensions(fileExtension: string) { const promptedFileExtensions = this.getPromptedFileExtensions(); promptedFileExtensions.push(fileExtension); - this.storageService.store(promptedFileExtensionsStorageKey, JSON.stringify(distinct(promptedFileExtensions)), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(promptedFileExtensionsStorageKey, JSON.stringify(distinct(promptedFileExtensions)), StorageScope.PROFILE, StorageTarget.USER); } private async promptRecommendedExtensionForFileExtension(uri: URI, fileExtension: string, installed: IExtension[]): Promise<void> { @@ -320,7 +322,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return; } - const fileExtensionSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]')); + const fileExtensionSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.PROFILE, '[]')); if (fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1) { return; } @@ -363,7 +365,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { this.storageService.store( 'extensionsAssistant/fileExtensionsSuggestionIgnore', JSON.stringify(fileExtensionSuggestionIgnoreList), - StorageScope.GLOBAL, + StorageScope.PROFILE, StorageTarget.USER); this.telemetryService.publicLog2<{ userReaction: string; fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'neverShowAgain', fileExtension }); } @@ -393,7 +395,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } private getCachedRecommendations(): IStringDictionary<number> { - let storedRecommendations = JSON.parse(this.storageService.get(recommendationsStorageKey, StorageScope.GLOBAL, '[]')); + let storedRecommendations = JSON.parse(this.storageService.get(recommendationsStorageKey, StorageScope.PROFILE, '[]')); if (Array.isArray(storedRecommendations)) { storedRecommendations = storedRecommendations.reduce((result, id) => { result[id] = Date.now(); return result; }, <IStringDictionary<number>>{}); } @@ -409,6 +411,6 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private storeCachedRecommendations(): void { const storedRecommendations: IStringDictionary<number> = {}; this.fileBasedRecommendations.forEach((value, key) => storedRecommendations[key] = value.recommendedTime); - this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.PROFILE, StorageTarget.MACHINE); } } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 78e7107e67c..f2a228fbd5b 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -82,6 +82,10 @@ overflow: hidden; } +.extension-list-item.deprecated > .details > .header-container > .header > .name { + text-decoration: line-through; +} + .extension-list-item > .details > .header-container > .header > .activation-status, .extension-list-item > .details > .header-container > .header > .install-count, .extension-list-item > .details > .header-container > .header > .ratings { @@ -152,6 +156,10 @@ color: var(--vscode-descriptionForeground); } +.monaco-list-row.selected .extension-list-item > .details > .description{ + color: unset; +} + .hc-black .extension-list-item > .details > .description, .hc-light .extension-list-item > .details > .description { color: unset; @@ -207,6 +215,14 @@ min-width: 0; } +.monaco-list-row.disabled .extension-list-item .details .description { + color: var(--vscode-disabledForeground); +} + +.monaco-list-row.disabled.selected .extension-list-item .details .description { + color: unset; +} + .extension-list-item .monaco-action-bar .action-label.icon { padding: 1px 2px; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index 75327c88344..13d0d87abca 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -52,6 +52,7 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.uninstall:not(.uninstalling), .monaco-action-bar .action-item.disabled .action-label.extension-action.hide-when-disabled, .monaco-action-bar .action-item.disabled .action-label.extension-action.update, +.monaco-action-bar .action-item.disabled .action-label.extension-action.migrate, .monaco-action-bar .action-item.disabled .action-label.extension-action.theme, .monaco-action-bar .action-item.disabled .action-label.extension-action.extension-sync, .monaco-action-bar .action-item.action-dropdown-item.disabled, diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 589bbe1742d..d011f17eca5 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -75,6 +75,10 @@ white-space: nowrap; } +.extension-editor > .header > .details > .title > .name.deprecated { + text-decoration: line-through; +} + .extension-editor > .header > .details > .title > .version { margin-left: 10px; user-select: text; @@ -149,11 +153,16 @@ .extension-editor > .header > .details > .subtitle, .extension-editor > .header > .details > .subtitle .install, -.extension-editor > .header > .details > .subtitle .rating { +.extension-editor > .header > .details > .subtitle .rating, +.extension-editor > .header > .details > .subtitle .sponsor { display: flex; align-items: center; } +.extension-editor > .header > .details > .subtitle .sponsor .codicon { + padding-right: 3px; +} + .extension-editor > .header > .details > .subtitle .install > .count { margin-left: 6px; } @@ -245,49 +254,36 @@ .extension-editor > .header > .details > .actions-status-container > .status { line-height: 22px; font-size: 90%; - display: flex; + margin-top: 3px; } .extension-editor > .header > .details > .actions-status-container.list-layout > .status { margin-top: 5px; } -.extension-editor > .header > .details > .actions-status-container > .status > .monaco-action-bar { - height: 22px; - margin-right: 2px; -} - -.extension-editor > .header > .details > .actions-status-container > .status > .monaco-action-bar .extension-action { - margin-top: 3px; -} - -.extension-editor > .header > .details > .actions-status-container.list-layout > .status > .monaco-action-bar .extension-action { - margin-top: 0px; -} - -.extension-editor > .header > .details > .actions-status-container:not(.list-layout) > .status > .status-text { - margin-top: 2px; +.extension-editor > .header > .details > .actions-status-container > .status .codicon { + vertical-align: text-bottom; } .extension-editor > .header > .details > .pre-release-text p, -.extension-editor > .header > .details > .actions-status-container > .status > .status-text p { +.extension-editor > .header > .details > .actions-status-container > .status p { margin-top: 0px; margin-bottom: 0px; } .extension-editor > .header > .details > .pre-release-text a, -.extension-editor > .header > .details > .actions-status-container > .status > .status-text a { +.extension-editor > .header > .details > .actions-status-container > .status a { color: var(--vscode-textLink-foreground) } .extension-editor > .header > .details > .pre-release-text a:hover, -.extension-editor > .header > .details > .actions-status-container > .status > .status-text a:hover { +.extension-editor > .header > .details > .actions-status-container > .status a:hover { text-decoration: underline; color: var(--vscode-textLink-activeForeground) } .extension-editor > .header > .details > .pre-release-text a:active, -.extension-editor > .header > .details > .actions-status-container > .status > .status-text a:active { +.extension-editor > .header > .details > .actions-status-container > .status a:active { color: var(--vscode-textLink-activeForeground) } @@ -408,15 +404,21 @@ box-sizing: border-box; } -.extension-editor > .body > .content > .details > .additional-details-container .additional-details-title { - font-size: 120%; +.extension-editor > .body > .content > .details > .additional-details-container .additional-details-element:not(:first-child) { + padding-top: 15px; } -.extension-editor > .body > .content > .details > .additional-details-container .categories-container > .categories, -.extension-editor > .body > .content > .details > .additional-details-container .tags-container > .tags, -.extension-editor > .body > .content > .details > .additional-details-container .resources-container > .resources, -.extension-editor > .body > .content > .details > .additional-details-container .more-info-container > .more-info { - margin: 15px 0px; +.extension-editor > .body > .content > .details > .additional-details-container .additional-details-element { + padding-bottom: 15px; +} + +.extension-editor > .body > .content > .details > .additional-details-container .additional-details-element:not(:last-child) { + border-bottom: 1px solid rgba(128, 128, 128, 0.22); +} + +.extension-editor > .body > .content > .details > .additional-details-container .additional-details-element > .additional-details-title { + font-size: 120%; + padding-bottom: 15px; } .extension-editor > .body > .content > .details > .additional-details-container .categories-container > .categories > .category, diff --git a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts index 5c65ba705c2..7c37b541e8a 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts @@ -13,7 +13,7 @@ export class Query { } static suggestions(query: string): string[] { - const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'featured', 'popular', 'recommended', 'workspaceUnsupported', 'sort', 'category', 'tag', 'ext', 'id'] as const; + const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'featured', 'popular', 'recommended', 'workspaceUnsupported', 'deprecated', 'sort', 'category', 'tag', 'ext', 'id'] as const; const subcommands = { 'sort': ['installs', 'rating', 'name', 'publishedDate'], 'category': EXTENSION_CATEGORIES.map(c => `"${c.toLowerCase()}"`), diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 47be2a6e355..60c5b415c54 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -9,7 +9,7 @@ import { IPager } from 'vs/base/common/paging'; import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, InstallOptions, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; @@ -17,6 +17,7 @@ import { IView, IViewPaneContainer } from 'vs/workbench/common/views'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionsStatus } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; export const VIEWLET_ID = 'workbench.view.extensions'; @@ -46,7 +47,9 @@ export interface IExtension { readonly identifier: IExtensionIdentifier; readonly publisher: string; readonly publisherDisplayName: string; + readonly publisherUrl?: URI; readonly publisherDomain?: { link: string; verified: boolean }; + readonly publisherSponsorLink?: URI; readonly version: string; readonly latestVersion: string; readonly hasPreReleaseVersion: boolean; @@ -88,6 +91,7 @@ export const IExtensionsWorkbenchService = createDecorator<IExtensionsWorkbenchS export interface IExtensionsWorkbenchService { readonly _serviceBrand: undefined; readonly onChange: Event<IExtension | undefined>; + readonly onReset: Event<void>; readonly preferPreReleases: boolean; readonly local: IExtension[]; readonly installed: IExtension[]; @@ -99,7 +103,7 @@ export interface IExtensionsWorkbenchService { getExtensions(extensionInfos: IExtensionInfo[], options: IExtensionQueryOptions, token: CancellationToken): Promise<IExtension[]>; canInstall(extension: IExtension): Promise<boolean>; install(vsix: URI, installOptions?: InstallVSIXOptions): Promise<IExtension>; - install(extension: IExtension, installOptions?: InstallOptions): Promise<IExtension>; + install(extension: IExtension, installOptions?: InstallOptions, progressLocation?: ProgressLocation): Promise<IExtension>; uninstall(extension: IExtension): Promise<void>; installVersion(extension: IExtension, version: string, installOptions?: InstallOptions): Promise<IExtension>; reinstall(extension: IExtension): Promise<IExtension>; @@ -134,7 +138,7 @@ export interface IExtensionsConfiguration { closeExtensionDetailsOnViewChange: boolean; } -export interface IExtensionContainer { +export interface IExtensionContainer extends IDisposable { extension: IExtension | null; updateWhenCounterExtensionChanges?: boolean; update(): void; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts index 44295a6868b..6e07fd90be6 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts @@ -92,9 +92,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio const timeStarted = Date.now(); const handle = setInterval(() => { - if (this.profilingStatusBarIndicator) { - this.profilingStatusBarIndicator.update({ ...indicator, text: nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), }); - } + this.profilingStatusBarIndicator?.update({ ...indicator, text: nls.localize('profilingExtensionHostTime', "Profiling Extension Host ({0} sec)", Math.round((new Date().getTime() - timeStarted) / 1000)), }); }, 1000); this.profilingStatusBarIndicatorLabelUpdater.value = toDisposable(() => clearInterval(handle)); diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts index 53ff6a67325..7a9ef97ade4 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts @@ -111,8 +111,8 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont let data: NamedSlice[] = []; for (let i = 0; i < profile.ids.length; i++) { - let id = profile.ids[i]; - let total = profile.deltas[i]; + const id = profile.ids[i]; + const total = profile.deltas[i]; data.push({ id, total, percentage: 0 }); } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts index 13380b3aaf5..7209af8ccaf 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts @@ -14,6 +14,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IStorageService, IS_NEW_KEY, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { AbstractExtensionsInitializer } from 'vs/platform/userDataSync/common/extensionsSync'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { IRemoteUserData, IUserDataSyncStoreManagementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; @@ -56,11 +57,11 @@ export class RemoteExtensionsInitializerContribution implements IWorkbenchContri } const newRemoteConnectionKey = `${IS_NEW_KEY}.${connection.remoteAuthority}`; // Skip: Not a new remote connection - if (!this.storageService.getBoolean(newRemoteConnectionKey, StorageScope.GLOBAL, true)) { + if (!this.storageService.getBoolean(newRemoteConnectionKey, StorageScope.APPLICATION, true)) { this.logService.trace(`Skipping initializing remote extensions because the window with this remote authority was opened before.`); return; } - this.storageService.store(newRemoteConnectionKey, false, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(newRemoteConnectionKey, false, StorageScope.APPLICATION, StorageTarget.MACHINE); // Skip: Not a new workspace if (!this.storageService.isNew(StorageScope.WORKSPACE)) { this.logService.trace(`Skipping initializing remote extensions because this workspace was opened before.`); @@ -99,13 +100,14 @@ class RemoteExtensionsInitializer extends AbstractExtensionsInitializer { @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IIgnoredExtensionsManagementService ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IFileService fileService: IFileService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IEnvironmentService environmentService: IEnvironmentService, @ILogService logService: ILogService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(extensionManagementService, ignoredExtensionsManagementService, fileService, environmentService, logService, uriIdentityService); + super(extensionManagementService, ignoredExtensionsManagementService, fileService, userDataProfilesService, environmentService, logService, uriIdentityService); } protected override async doInitialize(remoteUserData: IRemoteUserData): Promise<void> { diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction.ts index 9e85ffd28e4..36b988310c1 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/reportExtensionIssueAction.ts @@ -63,9 +63,9 @@ export class ReportExtensionIssueAction extends Action { baseUrl = this.productService.reportIssueUrl!; } - let reason = 'Bug'; - let title = 'Extension issue'; - let message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:'; + const reason = 'Bug'; + const title = 'Extension issue'; + const message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:'; this.clipboardService.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```'); const os = await this.nativeHostService.getOSProperties(); diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index b53ea1625f5..36170a31d5f 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -191,9 +191,9 @@ export class SaveExtensionHostProfileAction extends Action { } private async _asyncRun(): Promise<any> { - let picked = await this._nativeHostService.showSaveDialog({ - title: 'Save Extension Host Profile', - buttonLabel: 'Save', + const picked = await this._nativeHostService.showSaveDialog({ + title: nls.localize('saveprofile.dialogTitle', "Save Extension Host Profile"), + buttonLabel: nls.localize('saveprofile.saveButton', "Save"), defaultPath: `CPU-${new Date().toISOString().replace(/[\-:]/g, '')}.cpuprofile`, filters: [{ name: 'CPU Profiles', diff --git a/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts b/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts index d296d20c891..53a83c27df2 100644 --- a/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts +++ b/src/vs/workbench/contrib/extensions/test/common/extensionQuery.test.ts @@ -125,7 +125,7 @@ suite('Extension query', () => { }); test('equals', () => { - let query1 = new Query('hello', '', ''); + const query1 = new Query('hello', '', ''); let query2 = new Query('hello', '', ''); assert(query1.equals(query2)); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index add1a477c3f..6040e37a790 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import * as uuid from 'vs/base/common/uuid'; import { IExtensionGalleryService, IGalleryExtensionAssets, IGalleryExtension, IExtensionManagementService, - DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, IExtensionTipsService, InstallExtensionResult, getTargetPlatform + DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionTipsService, InstallExtensionResult, getTargetPlatform, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; @@ -191,10 +191,10 @@ suite('ExtensionRecommendationsService Test', () => { let testObject: ExtensionRecommendationsService; let installEvent: Emitter<InstallExtensionEvent>, didInstallEvent: Emitter<readonly InstallExtensionResult[]>, - uninstallEvent: Emitter<IExtensionIdentifier>, + uninstallEvent: Emitter<UninstallExtensionEvent>, didUninstallEvent: Emitter<DidUninstallExtensionEvent>; let prompted: boolean; - let promptedEmitter = new Emitter<void>(); + const promptedEmitter = new Emitter<void>(); let onModelAddedEvent: Emitter<ITextModel>; let experimentService: TestExperimentService; @@ -202,7 +202,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService = new TestInstantiationService(); installEvent = new Emitter<InstallExtensionEvent>(); didInstallEvent = new Emitter<readonly InstallExtensionResult[]>(); - uninstallEvent = new Emitter<IExtensionIdentifier>(); + uninstallEvent = new Emitter<UninstallExtensionEvent>(); didUninstallEvent = new Emitter<DidUninstallExtensionEvent>(); instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); @@ -414,8 +414,8 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: No Recommendations of globally ignored recommendations', () => { instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE); - instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]', StorageScope.GLOBAL, StorageTarget.MACHINE); - instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.GLOBAL, StorageTarget.MACHINE); + instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]', StorageScope.PROFILE, StorageTarget.MACHINE); + instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.PROFILE, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(TestExtensionRecommendationsService); @@ -433,7 +433,7 @@ suite('ExtensionRecommendationsService Test', () => { const ignoredRecommendations = ['ms-dotnettools.csharp', 'mockpublisher2.mockextension2']; // ignore a stored recommendation and a workspace recommendation. const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]'; instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE); - instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); + instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { testObject = instantiationService.createInstance(TestExtensionRecommendationsService); @@ -454,8 +454,8 @@ suite('ExtensionRecommendationsService Test', () => { const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]'; const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation. storageService.store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE); - storageService.store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); - storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); + storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations); testObject = instantiationService.createInstance(TestExtensionRecommendationsService); @@ -473,8 +473,8 @@ suite('ExtensionRecommendationsService Test', () => { const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]'; const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation. storageService.store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE); - storageService.store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); - storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); + storageService.store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions); const extensionIgnoredRecommendationsService = instantiationService.get(IExtensionIgnoredRecommendationsService); @@ -507,7 +507,7 @@ suite('ExtensionRecommendationsService Test', () => { const ignoredExtensionId = 'Some.Extension'; storageService.store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE, StorageTarget.MACHINE); - storageService.store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.PROFILE, StorageTarget.MACHINE); await setUpFolderWorkspace('myFolder', []); testObject = instantiationService.createInstance(TestExtensionRecommendationsService); @@ -522,7 +522,7 @@ suite('ExtensionRecommendationsService Test', () => { test('ExtensionRecommendationsService: Get file based recommendations from storage (old format)', () => { const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]'; - instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); + instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(TestExtensionRecommendationsService); @@ -541,7 +541,7 @@ suite('ExtensionRecommendationsService Test', () => { const now = Date.now(); const tenDaysOld = 10 * milliSecondsInADay; const storedRecommendations = `{"ms-dotnettools.csharp": ${now}, "ms-python.python": ${now}, "ms-vscode.vscode-typescript-tslint-plugin": ${now}, "lukehoban.Go": ${tenDaysOld}}`; - instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL, StorageTarget.MACHINE); + instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.PROFILE, StorageTarget.MACHINE); return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(TestExtensionRecommendationsService); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 93b1ba0e871..5b8e51532ed 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -10,9 +10,9 @@ import * as ExtensionsActions from 'vs/workbench/contrib/extensions/browser/exte import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, - DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata, InstallExtensionResult, getTargetPlatform, IExtensionsControlManifest + DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata, InstallExtensionResult, getTargetPlatform, IExtensionsControlManifest, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, ExtensionInstallLocation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, ExtensionInstallLocation, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; @@ -63,7 +63,7 @@ import { arch } from 'vs/base/common/process'; let instantiationService: TestInstantiationService; let installEvent: Emitter<InstallExtensionEvent>, didInstallEvent: Emitter<readonly InstallExtensionResult[]>, - uninstallEvent: Emitter<IExtensionIdentifier>, + uninstallEvent: Emitter<UninstallExtensionEvent>, didUninstallEvent: Emitter<DidUninstallExtensionEvent>; let disposables: DisposableStore; @@ -72,7 +72,7 @@ async function setupTest() { disposables = new DisposableStore(); installEvent = new Emitter<InstallExtensionEvent>(); didInstallEvent = new Emitter<readonly InstallExtensionResult[]>(); - uninstallEvent = new Emitter<IExtensionIdentifier>(); + uninstallEvent = new Emitter<UninstallExtensionEvent>(); didUninstallEvent = new Emitter<DidUninstallExtensionEvent>(); instantiationService = new TestInstantiationService(); @@ -99,6 +99,7 @@ async function setupTest() { onDidInstallExtensions: didInstallEvent.event, onUninstallExtension: uninstallEvent.event, onDidUninstallExtension: didUninstallEvent.event, + onDidChangeProfileExtensions: Event.None, async getInstalled() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {} }; }, async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata) { @@ -113,7 +114,7 @@ async function setupTest() { instantiationService.stub(IRemoteAgentService, RemoteAgentService); - const localExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' }; + const localExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService, label: 'local', id: 'vscode-local' }; instantiationService.stub(IExtensionManagementServerService, <Partial<IExtensionManagementServerService>>{ get localExtensionManagementServer(): IExtensionManagementServer { return localExtensionManagementServer; @@ -216,7 +217,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); @@ -231,7 +232,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); @@ -254,7 +255,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { testObject.extension = extensions[0]; - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); assert.ok(!testObject.enabled); assert.strictEqual('Uninstalling', testObject.label); assert.strictEqual('extension-action label uninstall uninstalling', testObject.class); @@ -308,23 +309,23 @@ suite('ExtensionsActions', () => { }); }); - test('Test Uninstall action after extension is installed', () => { + test('Test Uninstall action after extension is installed', async () => { const testObject: ExtensionsActions.UninstallAction = instantiationService.createInstance(ExtensionsActions.UninstallAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) - .then(paged => { - testObject.extension = paged.firstPage[0]; + const paged = await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); + testObject.extension = paged.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + installEvent.fire({ identifier: gallery.identifier, source: gallery }); + const promise = Event.toPromise(testObject.onDidChange); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); - assert.ok(testObject.enabled); - assert.strictEqual('Uninstall', testObject.label); - assert.strictEqual('extension-action label uninstall', testObject.class); - }); + await promise; + assert.ok(testObject.enabled); + assert.strictEqual('Uninstall', testObject.label); + assert.strictEqual('extension-action label uninstall', testObject.class); }); test('Test UpdateAction when there is no extension', () => { @@ -475,22 +476,22 @@ suite('ExtensionsActions', () => { }); }); - test('Test ManageExtensionAction when extension is queried from gallery and installed', () => { + test('Test ManageExtensionAction when extension is queried from gallery and installed', async () => { const testObject: ExtensionsActions.ManageExtensionAction = instantiationService.createInstance(ExtensionsActions.ManageExtensionAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) - .then(page => { - testObject.extension = page.firstPage[0]; - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + const paged = await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None); + testObject.extension = paged.firstPage[0]; + installEvent.fire({ identifier: gallery.identifier, source: gallery }); + const promise = Event.toPromise(testObject.onDidChange); + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); - assert.ok(testObject.enabled); - assert.strictEqual('extension-action icon manage codicon codicon-extensions-manage', testObject.class); - assert.strictEqual('', testObject.tooltip); - }); + await promise; + assert.ok(testObject.enabled); + assert.strictEqual('extension-action icon manage codicon codicon-extensions-manage', testObject.class); + assert.strictEqual('', testObject.tooltip); }); test('Test ManageExtensionAction when extension is system extension', () => { @@ -517,7 +518,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { testObject.extension = extensions[0]; - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); assert.ok(!testObject.enabled); assert.strictEqual('extension-action icon manage codicon codicon-extensions-manage', testObject.class); @@ -736,7 +737,7 @@ suite('ExtensionsActions', () => { .then(extensions => { const testObject: ExtensionsActions.EnableDropDownAction = instantiationService.createInstance(ExtensionsActions.EnableDropDownAction); testObject.extension = extensions[0]; - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); assert.ok(!testObject.enabled); }); }); @@ -899,7 +900,7 @@ suite('ExtensionsActions', () => { const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); testObject.extension = extensions[0]; instantiationService.createInstance(ExtensionContainers, [testObject]); - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); assert.ok(!testObject.enabled); }); }); @@ -939,7 +940,7 @@ suite('ReloadAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); assert.ok(!testObject.enabled); }); @@ -961,7 +962,9 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); installEvent.fire({ identifier: gallery.identifier, source: gallery }); + const promise = Event.toPromise(testObject.onDidChange); didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); + await promise; assert.ok(testObject.enabled); assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); }); @@ -1000,7 +1003,7 @@ suite('ReloadAction', () => { const identifier = gallery.identifier; installEvent.fire({ identifier, source: gallery }); didInstallEvent.fire([{ identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, { identifier }) }]); - uninstallEvent.fire(identifier); + uninstallEvent.fire({ identifier }); didUninstallEvent.fire({ identifier }); assert.ok(!testObject.enabled); @@ -1015,7 +1018,7 @@ suite('ReloadAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(testObject.enabled); assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to complete the uninstallation of this extension.'); @@ -1035,7 +1038,7 @@ suite('ReloadAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(!testObject.enabled); }); @@ -1049,7 +1052,7 @@ suite('ReloadAction', () => { const extensions = await instantiationService.get(IExtensionsWorkbenchService).queryLocal(); testObject.extension = extensions[0]; - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); const gallery = aGalleryExtension('a'); @@ -1248,7 +1251,7 @@ suite('ReloadAction', () => { const localExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a') }); const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); const localExtensionManagementService = createExtensionManagementService([localExtension]); - const uninstallEvent = new Emitter<IExtensionIdentifier>(); + const uninstallEvent = new Emitter<UninstallExtensionEvent>(); const onDidUninstallEvent = new Emitter<{ identifier: IExtensionIdentifier }>(); localExtensionManagementService.onUninstallExtension = uninstallEvent.event; localExtensionManagementService.onDidUninstallExtension = onDidUninstallEvent.event; @@ -1275,7 +1278,7 @@ suite('ReloadAction', () => { assert.ok(testObject.extension); assert.ok(!testObject.enabled); - uninstallEvent.fire(localExtension.identifier); + uninstallEvent.fire({ identifier: localExtension.identifier }); didUninstallEvent.fire({ identifier: localExtension.identifier }); assert.ok(!testObject.enabled); @@ -1311,8 +1314,10 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); const remoteExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file('pub.a').with({ scheme: Schemas.vscodeRemote }) }); + const promise = Event.toPromise(testObject.onDidChange); onDidInstallEvent.fire([{ identifier: remoteExtension.identifier, local: remoteExtension, operation: InstallOperation.Install }]); + await promise; assert.ok(testObject.enabled); assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); }); @@ -1347,8 +1352,10 @@ suite('ReloadAction', () => { assert.ok(!testObject.enabled); const localExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file('pub.a') }); + const promise = Event.toPromise(Event.filter(testObject.onDidChange, () => testObject.enabled)); onDidInstallEvent.fire([{ identifier: localExtension.identifier, local: localExtension, operation: InstallOperation.Install }]); + await promise; assert.ok(testObject.enabled); assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); }); @@ -1537,7 +1544,7 @@ suite('RemoteInstallAction', () => { test('Test remote install action when installing local workspace extension', async () => { // multi server setup - const remoteExtensionManagementService: IExtensionManagementService = createExtensionManagementService(); + const remoteExtensionManagementService = createExtensionManagementService(); const onInstallExtension = new Emitter<InstallExtensionEvent>(); remoteExtensionManagementService.onInstallExtension = onInstallExtension.event; const localWorkspaceExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); @@ -1568,7 +1575,7 @@ suite('RemoteInstallAction', () => { test('Test remote install action when installing local workspace extension is finished', async () => { // multi server setup - const remoteExtensionManagementService: IExtensionManagementService = createExtensionManagementService(); + const remoteExtensionManagementService = createExtensionManagementService(); const onInstallExtension = new Emitter<InstallExtensionEvent>(); remoteExtensionManagementService.onInstallExtension = onInstallExtension.event; const onDidInstallEvent = new Emitter<readonly InstallExtensionResult[]>(); @@ -1599,7 +1606,9 @@ suite('RemoteInstallAction', () => { assert.strictEqual('extension-action label install installing', testObject.class); const installedExtension = aLocalExtension('a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + const promise = Event.toPromise(testObject.onDidChange); onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install }]); + await promise; assert.ok(!testObject.enabled); }); @@ -1769,7 +1778,7 @@ suite('RemoteInstallAction', () => { test('Test remote install action is disabled for local workspace extension if it is uninstalled locally', async () => { // multi server setup - const extensionManagementService = instantiationService.get(IExtensionManagementService); + const extensionManagementService = instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, extensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); @@ -1788,7 +1797,7 @@ suite('RemoteInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - uninstallEvent.fire(localWorkspaceExtension.identifier); + uninstallEvent.fire({ identifier: localWorkspaceExtension.identifier }); assert.ok(!testObject.enabled); }); @@ -1913,7 +1922,7 @@ suite('RemoteInstallAction', () => { test('Test remote install action is disabled if local language pack extension is uninstalled', async () => { // multi server setup - const extensionManagementService = instantiationService.get(IExtensionManagementService); + const extensionManagementService = instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, extensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); @@ -1932,7 +1941,7 @@ suite('RemoteInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install in remote', testObject.label); - uninstallEvent.fire(languagePackExtension.identifier); + uninstallEvent.fire({ identifier: languagePackExtension.identifier }); assert.ok(!testObject.enabled); }); }); @@ -1986,7 +1995,7 @@ suite('LocalInstallAction', () => { test('Test local install action when installing remote ui extension', async () => { // multi server setup - const localExtensionManagementService: IExtensionManagementService = createExtensionManagementService(); + const localExtensionManagementService = createExtensionManagementService(); const onInstallExtension = new Emitter<InstallExtensionEvent>(); localExtensionManagementService.onInstallExtension = onInstallExtension.event; const remoteUIExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); @@ -2017,7 +2026,7 @@ suite('LocalInstallAction', () => { test('Test local install action when installing remote ui extension is finished', async () => { // multi server setup - const localExtensionManagementService: IExtensionManagementService = createExtensionManagementService(); + const localExtensionManagementService = createExtensionManagementService(); const onInstallExtension = new Emitter<InstallExtensionEvent>(); localExtensionManagementService.onInstallExtension = onInstallExtension.event; const onDidInstallEvent = new Emitter<readonly InstallExtensionResult[]>(); @@ -2048,7 +2057,9 @@ suite('LocalInstallAction', () => { assert.strictEqual('extension-action label install installing', testObject.class); const installedExtension = aLocalExtension('a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) }); + const promise = Event.toPromise(testObject.onDidChange); onDidInstallEvent.fire([{ identifier: installedExtension.identifier, local: installedExtension, operation: InstallOperation.Install }]); + await promise; assert.ok(!testObject.enabled); }); @@ -2178,7 +2189,7 @@ suite('LocalInstallAction', () => { test('Test local install action is disabled for remoteUI extension if it is uninstalled locally', async () => { // multi server setup - const extensionManagementService = instantiationService.get(IExtensionManagementService); + const extensionManagementService = instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), extensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); @@ -2197,7 +2208,7 @@ suite('LocalInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - uninstallEvent.fire(remoteUIExtension.identifier); + uninstallEvent.fire({ identifier: remoteUIExtension.identifier }); assert.ok(!testObject.enabled); }); @@ -2301,7 +2312,7 @@ suite('LocalInstallAction', () => { test('Test local install action is disabled if remote language pack extension is uninstalled', async () => { // multi server setup - const extensionManagementService = instantiationService.get(IExtensionManagementService); + const extensionManagementService = instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService; const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService(), extensionManagementService); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); @@ -2320,7 +2331,7 @@ suite('LocalInstallAction', () => { assert.ok(testObject.enabled); assert.strictEqual('Install Locally', testObject.label); - uninstallEvent.fire(languagePackExtension.identifier); + uninstallEvent.fire({ identifier: languagePackExtension.identifier }); assert.ok(!testObject.enabled); }); @@ -2352,7 +2363,7 @@ function aPage<T>(...objects: T[]): IPager<T> { return { firstPage: objects, total: objects.length, pageSize: objects.length, getPage: () => null! }; } -function aSingleRemoteExtensionManagementServerService(instantiationService: TestInstantiationService, remoteExtensionManagementService?: IExtensionManagementService): IExtensionManagementServerService { +function aSingleRemoteExtensionManagementServerService(instantiationService: TestInstantiationService, remoteExtensionManagementService?: IProfileAwareExtensionManagementService): IExtensionManagementServerService { const remoteExtensionManagementServer: IExtensionManagementServer = { id: 'vscode-remote', label: 'remote', @@ -2376,7 +2387,7 @@ function aSingleRemoteExtensionManagementServerService(instantiationService: Tes }; } -function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService, localExtensionManagementService?: IExtensionManagementService, remoteExtensionManagementService?: IExtensionManagementService): IExtensionManagementServerService { +function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService, localExtensionManagementService?: IProfileAwareExtensionManagementService, remoteExtensionManagementService?: IProfileAwareExtensionManagementService): IExtensionManagementServerService { const localExtensionManagementServer: IExtensionManagementServer = { id: 'vscode-local', label: 'local', @@ -2408,12 +2419,13 @@ function aMultiExtensionManagementServerService(instantiationService: TestInstan }; } -function createExtensionManagementService(installed: ILocalExtension[] = []): IExtensionManagementService { - return <IExtensionManagementService>{ +function createExtensionManagementService(installed: ILocalExtension[] = []): IProfileAwareExtensionManagementService { + return <IProfileAwareExtensionManagementService>{ onInstallExtension: Event.None, onDidInstallExtensions: Event.None, onUninstallExtension: Event.None, onDidUninstallExtension: Event.None, + onDidChangeProfileExtensions: Event.None, getInstalled: () => Promise.resolve<ILocalExtension[]>(installed), canInstall: async (extension: IGalleryExtension) => { return true; }, installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 1212a0ed3b2..8eeb60477c4 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -11,9 +11,9 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, - DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, SortBy, InstallExtensionResult, getTargetPlatform, IExtensionInfo + DidUninstallExtensionEvent, InstallExtensionEvent, SortBy, InstallExtensionResult, getTargetPlatform, IExtensionInfo, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService, ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; @@ -54,7 +54,7 @@ suite('ExtensionsListView Tests', () => { let testableView: ExtensionsListView; let installEvent: Emitter<InstallExtensionEvent>, didInstallEvent: Emitter<readonly InstallExtensionResult[]>, - uninstallEvent: Emitter<IExtensionIdentifier>, + uninstallEvent: Emitter<UninstallExtensionEvent>, didUninstallEvent: Emitter<DidUninstallExtensionEvent>; const localEnabledTheme = aLocalExtension('first-enabled-extension', { categories: ['Themes', 'random'] }); @@ -76,7 +76,7 @@ suite('ExtensionsListView Tests', () => { suiteSetup(() => { installEvent = new Emitter<InstallExtensionEvent>(); didInstallEvent = new Emitter<readonly InstallExtensionResult[]>(); - uninstallEvent = new Emitter<IExtensionIdentifier>(); + uninstallEvent = new Emitter<UninstallExtensionEvent>(); didUninstallEvent = new Emitter<DidUninstallExtensionEvent>(); instantiationService = new TestInstantiationService(); @@ -96,6 +96,7 @@ suite('ExtensionsListView Tests', () => { onDidInstallExtensions: didInstallEvent.event, onUninstallExtension: uninstallEvent.event, onDidUninstallExtension: didUninstallEvent.event, + onDidChangeProfileExtensions: Event.None, async getInstalled() { return []; }, async canInstall() { return true; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {} }; }, @@ -105,7 +106,7 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IMenuService, new TestMenuService()); - const localExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' }; + const localExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService, label: 'local', id: 'vscode-local' }; instantiationService.stub(IExtensionManagementServerService, <Partial<IExtensionManagementServerService>>{ get localExtensionManagementServer(): IExtensionManagementServer { return localExtensionManagementServer; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 0c7d5816688..f2aa03a1782 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -11,9 +11,9 @@ import { IExtensionsWorkbenchService, ExtensionState, AutoCheckUpdatesConfigurat import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, - DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata, InstallExtensionResult, getTargetPlatform, IExtensionsControlManifest + DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, InstallOperation, IExtensionTipsService, IGalleryMetadata, InstallExtensionResult, getTargetPlatform, IExtensionsControlManifest, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { anExtensionManagementServerService, TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; @@ -59,13 +59,13 @@ suite('ExtensionsWorkbenchServiceTest', () => { let installEvent: Emitter<InstallExtensionEvent>, didInstallEvent: Emitter<readonly InstallExtensionResult[]>, - uninstallEvent: Emitter<IExtensionIdentifier>, + uninstallEvent: Emitter<UninstallExtensionEvent>, didUninstallEvent: Emitter<DidUninstallExtensionEvent>; suiteSetup(() => { installEvent = new Emitter<InstallExtensionEvent>(); didInstallEvent = new Emitter<readonly InstallExtensionResult[]>(); - uninstallEvent = new Emitter<IExtensionIdentifier>(); + uninstallEvent = new Emitter<UninstallExtensionEvent>(); didUninstallEvent = new Emitter<DidUninstallExtensionEvent>(); instantiationService = new TestInstantiationService(); @@ -94,6 +94,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidInstallExtensions: didInstallEvent.event, onUninstallExtension: uninstallEvent.event, onDidUninstallExtension: didUninstallEvent.event, + onDidChangeProfileExtensions: Event.None, async getInstalled() { return []; }, async getExtensionsControlManifest() { return { malicious: [], deprecated: {} }; }, async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata) { @@ -109,7 +110,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ id: 'local', label: 'local', - extensionManagementService: instantiationService.get(IExtensionManagementService), + extensionManagementService: instantiationService.get(IExtensionManagementService) as IProfileAwareExtensionManagementService, }, null, null)); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); @@ -371,7 +372,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { // Installing installEvent.fire({ identifier, source: gallery }); - let local = testObject.local; + const local = testObject.local; assert.strictEqual(1, local.length); const actual = local[0]; assert.strictEqual(`${gallery.publisher}.${gallery.name}`, actual.identifier.id); @@ -385,7 +386,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject.uninstall(actual); // Uninstalling - uninstallEvent.fire(identifier); + uninstallEvent.fire({ identifier }); assert.strictEqual(ExtensionState.Uninstalling, actual.state); // Uninstalled @@ -412,7 +413,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const target = testObject.local[0]; testObject.uninstall(target); - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(!(await testObject.canInstall(target))); @@ -446,21 +447,19 @@ suite('ExtensionsWorkbenchServiceTest', () => { const gallery = aGalleryExtension('gallery1'); testObject = await aWorkbenchService(); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); - const target = sinon.spy(); - return testObject.queryGallery(CancellationToken.None).then(page => { - const extension = page.firstPage[0]; - assert.strictEqual(ExtensionState.Uninstalled, extension.state); + const page = await testObject.queryGallery(CancellationToken.None); + const extension = page.firstPage[0]; + assert.strictEqual(ExtensionState.Uninstalled, extension.state); - testObject.install(extension); - installEvent.fire({ identifier: gallery.identifier, source: gallery }); - testObject.onChange(target); + testObject.install(extension); + installEvent.fire({ identifier: gallery.identifier, source: gallery }); + const promise = Event.toPromise(testObject.onChange); - // Installed - didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, gallery) }]); + // Installed + didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension(gallery.name, gallery, gallery) }]); - assert.ok(target.calledOnce); - }); + await promise; }); test('test onchange event is triggered when installation is finished', async () => { @@ -491,7 +490,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject.uninstall(testObject.local[0]); testObject.onChange(target); - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); assert.ok(target.calledOnce); }); @@ -503,7 +502,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { const target = sinon.spy(); testObject.uninstall(testObject.local[0]); - uninstallEvent.fire(local.identifier); + uninstallEvent.fire({ identifier: local.identifier }); testObject.onChange(target); didUninstallEvent.fire({ identifier: local.identifier }); @@ -1460,7 +1459,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { }); } - function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService, localExtensionManagementService?: IExtensionManagementService, remoteExtensionManagementService?: IExtensionManagementService): IExtensionManagementServerService { + function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService, localExtensionManagementService?: IProfileAwareExtensionManagementService, remoteExtensionManagementService?: IProfileAwareExtensionManagementService): IExtensionManagementServerService { const localExtensionManagementServer: IExtensionManagementServer = { id: 'vscode-local', label: 'local', @@ -1474,12 +1473,13 @@ suite('ExtensionsWorkbenchServiceTest', () => { return anExtensionManagementServerService(localExtensionManagementServer, remoteExtensionManagementServer, null); } - function createExtensionManagementService(installed: ILocalExtension[] = []): IExtensionManagementService { - return <IExtensionManagementService>{ + function createExtensionManagementService(installed: ILocalExtension[] = []): IProfileAwareExtensionManagementService { + return <IProfileAwareExtensionManagementService>{ onInstallExtension: Event.None, onDidInstallExtensions: Event.None, onUninstallExtension: Event.None, onDidUninstallExtension: Event.None, + onDidChangeProfileExtensions: Event.None, getInstalled: () => Promise.resolve<ILocalExtension[]>(installed), installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), updateMetadata: async (local: ILocalExtension, metadata: IGalleryMetadata) => { diff --git a/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts index 6da2c2b1ad8..f1b4693c38c 100644 --- a/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution.ts @@ -89,7 +89,7 @@ export class ExternalTerminalContribution implements IWorkbenchContribution { private async _updateConfiguration(): Promise<void> { const terminals = await this._externalTerminalService.getDefaultTerminalForPlatforms(); - let configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration); + const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration); configurationRegistry.registerConfiguration({ id: 'externalTerminal', order: 100, diff --git a/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts index 6b3229c7bf3..af945a428c6 100644 --- a/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts +++ b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts @@ -34,7 +34,7 @@ export class ContributedExternalUriOpenersStore extends Disposable { super(); this._memento = new Memento(ContributedExternalUriOpenersStore.STORAGE_ID, storageService); - this._mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + this._mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); for (const [id, value] of Object.entries(this._mementoObject || {})) { this.add(id, value.extensionId, { isCurrentlyRegistered: false }); } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index e77d66da61a..896287bf204 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -9,8 +9,8 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IAction, toAction } from 'vs/base/common/actions'; 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, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; +import { AbstractTextCodeEditor } from 'vs/workbench/browser/parts/editor/textCodeEditor'; +import { IEditorOpenContext, 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'; @@ -30,7 +30,6 @@ import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { MutableDisposable } from 'vs/base/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -38,15 +37,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur /** * An implementation of editor for file system resources. */ -export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { +export class TextFileEditor extends AbstractTextCodeEditor<ICodeEditorViewState> { static readonly ID = TEXT_FILE_EDITOR_ID; - private readonly inputListener = this._register(new MutableDisposable()); - constructor( @ITelemetryService telemetryService: ITelemetryService, - @IFileService private readonly fileService: IFileService, + @IFileService fileService: IFileService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -61,17 +58,13 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { @IPathService private readonly pathService: IPathService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); + super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); // Clear view state for deleted files this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); // Move view state for moved files this._register(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e))); - - // Listen to file system provider changes - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); } private onDidFilesChange(e: FileChangesEvent): void { @@ -86,27 +79,12 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { } } - private onDidChangeFileSystemProvider(scheme: string): void { - if (this.input?.resource.scheme === scheme) { - this.updateReadonly(this.input); - } - } - - private onDidChangeInputCapabilities(input: FileEditorInput): void { - if (this.input === input) { - this.updateReadonly(input); - } - } - - private updateReadonly(input: FileEditorInput): void { - const control = this.getControl(); - if (control) { - control.updateOptions({ readOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); + override getTitle(): string { + if (this.input) { + return this.input.getName(); } - } - override getTitle(): string { - return this.input ? this.input.getName() : localize('textFileEditor', "Text File Editor"); + return localize('textFileEditor', "Text File Editor"); } override get input(): FileEditorInput | undefined { @@ -115,9 +93,6 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { override async setInput(input: FileEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { - // Update our listener for input capabilities - this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); - // Set input and resolve await super.setInput(input, options, context, token); try { @@ -138,8 +113,8 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { const textFileModel = resolvedModel; // Editor - const textEditor = assertIsDefined(this.getControl()); - textEditor.setModel(textFileModel.textEditorModel); + const control = assertIsDefined(this.editorControl); + control.setModel(textFileModel.textEditorModel); // Restore view state (unless provided by options) if (!isTextEditorViewState(options?.viewState)) { @@ -149,13 +124,13 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { editorViewState.cursorState = []; // prevent duplicate selections via options } - textEditor.restoreViewState(editorViewState); + control.restoreViewState(editorViewState); } } // Apply options to editor if any if (options) { - applyTextEditorOptions(options, textEditor, ScrollType.Immediate); + applyTextEditorOptions(options, control, ScrollType.Immediate); } // Since the resolved model provides information about being readonly @@ -163,7 +138,7 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { // was already asked for being readonly or not. The rationale is that // a resolved model might have more specific information about being // readonly or not that the input did not have. - textEditor.updateOptions({ readOnly: textFileModel.isReadonly() }); + control.updateOptions({ readOnly: textFileModel.isReadonly() }); } catch (error) { await this.handleSetInputError(error, input, options); } @@ -228,7 +203,7 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { const defaultBinaryEditor = this.configurationService.getValue<string | undefined>('workbench.editor.defaultBinaryEditor'); const group = this.group ?? this.editorGroupService.activeGroup; - let editorOptions = { + const editorOptions = { ...options, // Make sure to not steal away the currently active group // because we are triggering another openEditor() call @@ -279,14 +254,8 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { override clearInput(): void { super.clearInput(); - // Clear input listener - this.inputListener.clear(); - // Clear Model - const textEditor = this.getControl(); - if (textEditor) { - textEditor.setModel(null); - } + this.editorControl?.setModel(null); } protected override tracksEditorViewState(input: EditorInput): boolean { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 95c4806a86f..659469ef968 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -110,7 +110,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa // If the user tried to save from the opened conflict editor, show its message again if (this.activeConflictResolutionResource && isEqual(this.activeConflictResolutionResource, model.resource)) { - if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) { + if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.APPLICATION)) { return; // return if this message is ignored } @@ -225,8 +225,8 @@ class DoNotShowResolveConflictLearnMoreAction extends Action { override async run(notification: IDisposable): Promise<void> { - // Remember this as global state - this.storageService.store(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, true, StorageScope.GLOBAL, StorageTarget.USER); + // Remember this as application state + this.storageService.store(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, true, StorageScope.APPLICATION, StorageTarget.USER); // Hide notification notification.dispose(); diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index a1d5910f284..bfa4654c1e2 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -125,10 +125,10 @@ export class ExplorerService implements IExplorerService { this.view.setTreeInput(); } })); + // Refresh explorer when window gets focus to compensate for missing file events #126817 - const skipRefreshExplorerOnWindowFocus = this.configurationService.getValue('skipRefreshExplorerOnWindowFocus'); this.disposables.add(hostService.onDidChangeFocus(hasFocus => { - if (!skipRefreshExplorerOnWindowFocus && hasFocus) { + if (hasFocus) { this.refresh(false); } })); @@ -156,7 +156,7 @@ export class ExplorerService implements IExplorerService { const items = new Set<ExplorerItem>(this.view.getContext(respectMultiSelection)); items.forEach(item => { - if (this.view?.isItemCollapsed(item) && item.nestedChildren) { + if (respectMultiSelection && this.view?.isItemCollapsed(item) && item.nestedChildren) { for (const child of item.nestedChildren) { items.add(child); } @@ -421,7 +421,7 @@ export class ExplorerService implements IExplorerService { } function doesFileEventAffect(item: ExplorerItem, view: IExplorerView, events: FileChangesEvent[], types: FileChangeType[]): boolean { - for (let [_name, child] of item.children) { + for (const [_name, child] of item.children) { if (view.isItemVisible(child)) { if (events.some(e => e.contains(child.resource, ...types))) { return true; diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 9f0e731c179..6188a5bcb86 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -67,8 +67,8 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor private registerViews(): void { const viewDescriptors = viewsRegistry.getViews(VIEW_CONTAINER); - let viewDescriptorsToRegister: IViewDescriptor[] = []; - let viewDescriptorsToDeregister: IViewDescriptor[] = []; + const viewDescriptorsToRegister: IViewDescriptor[] = []; + const viewDescriptorsToDeregister: IViewDescriptor[] = []; const openEditorsViewDescriptor = this.createOpenEditorsViewDescriptor(); if (!viewDescriptors.some(v => v.id === openEditorsViewDescriptor.id)) { @@ -224,9 +224,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { } const openEditorsView = this.getOpenEditorsView(); - if (openEditorsView) { - openEditorsView.setStructuralRefreshDelay(0); - } + openEditorsView?.setStructuralRefreshDelay(0); } }); } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 56160c6d47d..b90da455004 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -326,7 +326,7 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa const suffixRegex = /^(.+ copy)( \d+)?$/; if (suffixRegex.test(namePrefix)) { return namePrefix.replace(suffixRegex, (match, g1?, g2?) => { - let number = (g2 ? parseInt(g2) : 1); + const number = (g2 ? parseInt(g2) : 1); return number === 0 ? `${g1}` : (number < Constants.MAX_SAFE_SMALL_INTEGER @@ -343,10 +343,10 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa const maxNumber = Constants.MAX_SAFE_SMALL_INTEGER; // file.1.txt=>file.2.txt - let suffixFileRegex = RegExp('(.*' + separators + ')(\\d+)(\\..*)$'); + const suffixFileRegex = RegExp('(.*' + separators + ')(\\d+)(\\..*)$'); if (!isFolder && name.match(suffixFileRegex)) { return name.replace(suffixFileRegex, (match, g1?, g2?, g3?) => { - let number = parseInt(g2); + const number = parseInt(g2); return number < maxNumber ? g1 + String(number + 1).padStart(g2.length, '0') + g3 : `${g1}${g2}.1${g3}`; @@ -354,10 +354,10 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa } // 1.file.txt=>2.file.txt - let prefixFileRegex = RegExp('(\\d+)(' + separators + '.*)(\\..*)$'); + const prefixFileRegex = RegExp('(\\d+)(' + separators + '.*)(\\..*)$'); if (!isFolder && name.match(prefixFileRegex)) { return name.replace(prefixFileRegex, (match, g1?, g2?, g3?) => { - let number = parseInt(g1); + const number = parseInt(g1); return number < maxNumber ? String(number + 1).padStart(g1.length, '0') + g2 + g3 : `${g1}${g2}.1${g3}`; @@ -365,10 +365,10 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa } // 1.txt=>2.txt - let prefixFileNoNameRegex = RegExp('(\\d+)(\\..*)$'); + const prefixFileNoNameRegex = RegExp('(\\d+)(\\..*)$'); if (!isFolder && name.match(prefixFileNoNameRegex)) { return name.replace(prefixFileNoNameRegex, (match, g1?, g2?) => { - let number = parseInt(g1); + const number = parseInt(g1); return number < maxNumber ? String(number + 1).padStart(g1.length, '0') + g2 : `${g1}.1${g2}`; @@ -382,10 +382,10 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa } // 123 => 124 - let noNameNoExtensionRegex = RegExp('(\\d+)$'); + const noNameNoExtensionRegex = RegExp('(\\d+)$'); if (!isFolder && lastIndexOfDot === -1 && name.match(noNameNoExtensionRegex)) { return name.replace(noNameNoExtensionRegex, (match, g1?) => { - let number = parseInt(g1); + const number = parseInt(g1); return number < maxNumber ? String(number + 1).padStart(g1.length, '0') : `${g1}.1`; @@ -394,7 +394,7 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa // file => file1 // file1 => file2 - let noExtensionRegex = RegExp('(.*)(\\d*)$'); + const noExtensionRegex = RegExp('(.*)(\\d*)$'); if (!isFolder && lastIndexOfDot === -1 && name.match(noExtensionRegex)) { return name.replace(noExtensionRegex, (match, g1?, g2?) => { let number = parseInt(g2); @@ -410,7 +410,7 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa // folder.1=>folder.2 if (isFolder && name.match(/(\d+)$/)) { return name.replace(/(\d+)$/, (match, ...groups) => { - let number = parseInt(groups[0]); + const number = parseInt(groups[0]); return number < maxNumber ? String(number + 1).padStart(groups[0].length, '0') : `${groups[0]}.1`; @@ -420,7 +420,7 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa // 1.folder=>2.folder if (isFolder && name.match(/^(\d+)/)) { return name.replace(/^(\d+)(.*)$/, (match, ...groups) => { - let number = parseInt(groups[0]); + const number = parseInt(groups[0]); return number < maxNumber ? String(number + 1).padStart(groups[0].length, '0') + groups[1] : `${groups[0]}${groups[1]}.1`; diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 2215ecd93ad..bb2297d08c7 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -20,7 +20,7 @@ import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { extractEditorsDropData } from 'vs/platform/dnd/browser/dnd'; +import { extractEditorsAndFilesDropData } from 'vs/platform/dnd/browser/dnd'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { isWeb } from 'vs/base/common/platform'; import { triggerDownload } from 'vs/base/browser/dom'; @@ -427,7 +427,7 @@ export class ExternalFileImport { private async doImport(target: ExplorerItem, source: DragEvent, token: CancellationToken): Promise<void> { // Activate all providers for the resources dropped - const candidateFiles = coalesce((await this.instantiationService.invokeFunction(accessor => extractEditorsDropData(accessor, source))).map(editor => editor.resource)); + const candidateFiles = coalesce((await this.instantiationService.invokeFunction(accessor => extractEditorsAndFilesDropData(accessor, source))).map(editor => editor.resource)); await Promise.all(candidateFiles.map(resource => this.fileService.activateProvider(resource.scheme))); // Check for dropped external files to be folders @@ -796,7 +796,7 @@ export class FileDownload { progress.report({ message: explorerItem.name }); let defaultUri: URI; - const lastUsedDownloadPath = this.storageService.get(FileDownload.LAST_USED_DOWNLOAD_PATH_STORAGE_KEY, StorageScope.GLOBAL); + const lastUsedDownloadPath = this.storageService.get(FileDownload.LAST_USED_DOWNLOAD_PATH_STORAGE_KEY, StorageScope.APPLICATION); if (lastUsedDownloadPath) { defaultUri = joinPath(URI.file(lastUsedDownloadPath), explorerItem.name); } else { @@ -818,7 +818,7 @@ export class FileDownload { if (destination) { // Remember as last used download folder - this.storageService.store(FileDownload.LAST_USED_DOWNLOAD_PATH_STORAGE_KEY, dirname(destination).fsPath, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(FileDownload.LAST_USED_DOWNLOAD_PATH_STORAGE_KEY, dirname(destination).fsPath, StorageScope.APPLICATION, StorageTarget.MACHINE); // Perform download await this.explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index b162ad2ead5..592d559451a 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -148,6 +148,8 @@ configurationRegistry.registerConfiguration({ 'anyOf': [ { 'type': 'boolean', + 'enum': [true, false], + 'enumDescriptions': [nls.localize('trueDescription', "Enable the pattern."), nls.localize('falseDescription', "Disable the pattern.")], 'description': nls.localize('files.exclude.boolean', "The glob pattern to match file paths against. Set to true or false to enable or disable the pattern."), }, { @@ -471,6 +473,12 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('copyRelativePathSeparator', "The path separation character used when copying relative file paths."), 'default': 'auto' }, + 'explorer.excludeGitIgnore': { + type: 'boolean', + markdownDescription: nls.localize('excludeGitignore', "Controls whether entries in .gitignore should be parsed and excluded from the explorer. Similar to `#files.exclude#`."), + default: false, + scope: ConfigurationScope.RESOURCE + }, 'explorer.fileNesting.enabled': { 'type': 'boolean', scope: ConfigurationScope.RESOURCE, @@ -500,7 +508,7 @@ configurationRegistry.registerConfiguration({ '*.jsx': '${capture}.js', '*.tsx': '${capture}.ts', 'tsconfig.json': 'tsconfig.*.json', - 'package.json': 'package-lock.json, yarn.lock', + 'package.json': 'package-lock.json, yarn.lock, pnpm-lock.yaml', } } } diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index b4b01687527..c02da0474ac 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -23,7 +23,7 @@ export interface IExplorerService { readonly roots: ExplorerItem[]; readonly sortOrderConfiguration: ISortOrderConfiguration; - getContext(respectMultiSelection: boolean, includeNestedChildren?: boolean): ExplorerItem[]; + getContext(respectMultiSelection: boolean): ExplorerItem[]; hasViewFocus(): boolean; setEditable(stat: ExplorerItem, data: IEditableData | null): Promise<void>; getEditable(): { stat: ExplorerItem; data: IEditableData } | undefined; @@ -61,7 +61,7 @@ export interface IExplorerView { } function getFocus(listService: IListService): unknown | undefined { - let list = listService.lastFocusedList; + const list = listService.lastFocusedList; if (list?.getHTMLElement() === document.activeElement) { let focus: unknown; if (list instanceof List) { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts index 062c0d23e29..41f221e0db0 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts @@ -65,10 +65,10 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { return this._onDidChange.event; } - provideDecorations(resource: URI): IDecorationData | undefined { + async provideDecorations(resource: URI): Promise<IDecorationData | undefined> { const fileStat = this.explorerService.findClosest(resource); if (!fileStat) { - return undefined; + throw new Error('ExplorerItem not found'); } return provideDecorations(fileStat); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 1ba98e190c7..1e2e605ec5e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -84,10 +84,10 @@ function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree<ExplorerI const identityProvider = { getId: (stat: ExplorerItem) => { if (stat instanceof NewExplorerItem) { - return `new:${stat.resource}`; + return `new:${stat.getId()}`; } - return stat.resource; + return stat.getId(); } }; @@ -479,7 +479,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree.onDidScroll(async e => { - let editable = this.explorerService.getEditable(); + const editable = this.explorerService.getEditable(); if (e.scrollTopChanged && editable && this.tree.getRelativeTop(editable.stat) === null) { await editable.data.onFinish('', false); } @@ -489,9 +489,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { const element = e.node.element?.element; if (element) { const navigationController = this.renderer.getCompressedNavigationController(element instanceof Array ? element[0] : element); - if (navigationController) { - navigationController.updateCollapsed(e.node.collapsed); - } + navigationController?.updateCollapsed(e.node.collapsed); } })); @@ -538,7 +536,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { private async onContextMenu(e: ITreeContextMenuEvent<ExplorerItem>): Promise<void> { const disposables = new DisposableStore(); - let stat = e.element; + const stat = e.element; let anchor = e.anchor; // Compressed folders @@ -731,7 +729,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { return this.selectResource(resource, reveal, retry + 1); } - for (let child of item.children.values()) { + for (const child of item.children.values()) { if (this.uriIdentityService.extUri.isEqualOrParent(resource, child.resource)) { item = child; break; @@ -765,9 +763,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { itemsCopied(stats: ExplorerItem[], cut: boolean, previousCut: ExplorerItem[] | undefined): void { this.fileCopiedContextKey.set(stats.length > 0); this.resourceCutContextKey.set(cut && stats.length > 0); - if (previousCut) { - previousCut.forEach(item => this.tree.rerender(item)); - } + previousCut?.forEach(item => this.tree.rerender(item)); if (cut) { stats.forEach(s => this.tree.rerender(s)); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index a24f390cf51..33dd4174024 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -9,7 +9,7 @@ import * as glob from 'vs/base/common/glob'; import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; import { IProgressService, ProgressLocation, } from 'vs/platform/progress/common/progress'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IFileService, FileKind, FileOperationError, FileOperationResult, FileChangeType } from 'vs/platform/files/common/files'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -58,6 +58,8 @@ import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { BrowserFileUpload, ExternalFileImport, getMultipleFilesOverwriteConfirm } from 'vs/workbench/contrib/files/browser/fileImportExport'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; +import { IgnoreFile } from 'vs/workbench/services/search/common/ignoreFile'; +import { ResourceSet, TernarySearchTree } from 'vs/base/common/map'; export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> { @@ -605,20 +607,42 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> { private editorsAffectingFilter = new Set<EditorInput>(); private _onDidChange = new Emitter<void>(); private toDispose: IDisposable[] = []; + // List of ignoreFile resources. Used to detect changes to the ignoreFiles. + private ignoreFileResourcesPerRoot = new Map<string, ResourceSet>(); + // Ignore tree per root. Similar to `hiddenExpressionPerRoot` + // Note: URI in the ternary search tree is the URI of the folder containing the ignore file + // It is not the ignore file itself. This is because of the way the IgnoreFile works and nested paths + private ignoreTreesPerRoot = new Map<string, TernarySearchTree<URI, IgnoreFile>>(); constructor( @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExplorerService private readonly explorerService: IExplorerService, @IEditorService private readonly editorService: IEditorService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IFileService private readonly fileService: IFileService ) { this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration())); this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('files.exclude')) { + if (e.affectsConfiguration('files.exclude') || e.affectsConfiguration('explorer.excludeGitIgnore')) { this.updateConfiguration(); } })); + this.toDispose.push(this.fileService.onDidFilesChange(e => { + // Check to see if the update contains any of the ignoreFileResources + for (const [root, ignoreFileResourceSet] of this.ignoreFileResourcesPerRoot.entries()) { + ignoreFileResourceSet.forEach(async ignoreResource => { + if (e.contains(ignoreResource, FileChangeType.UPDATED)) { + await this.processIgnoreFile(root, ignoreResource, true); + } + if (e.contains(ignoreResource, FileChangeType.DELETED)) { + this.ignoreTreesPerRoot.get(root)?.delete(dirname(ignoreResource)); + ignoreFileResourceSet.delete(ignoreResource); + this._onDidChange.fire(); + } + }); + } + })); this.toDispose.push(this.editorService.onDidVisibleEditorsChange(() => { const editors = this.editorService.visibleEditors; let shouldFire = false; @@ -658,9 +682,25 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> { private updateConfiguration(): void { let shouldFire = false; + let updatedGitIgnoreSetting = false; this.contextService.getWorkspace().folders.forEach(folder => { const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri }); const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null); + const parseIgnoreFile: boolean = configuration.explorer.excludeGitIgnore; + + // If we should be parsing ignoreFiles for this workspace and don't have an ignore tree initialize one + if (parseIgnoreFile && !this.ignoreTreesPerRoot.has(folder.uri.toString())) { + updatedGitIgnoreSetting = true; + this.ignoreFileResourcesPerRoot.set(folder.uri.toString(), new ResourceSet()); + this.ignoreTreesPerRoot.set(folder.uri.toString(), TernarySearchTree.forUris((uri) => this.uriIdentityService.extUri.ignorePathCasing(uri))); + } + + // If we shouldn't be parsing ignore files but have an ignore tree, clear the ignore tree + if (!parseIgnoreFile && this.ignoreTreesPerRoot.has(folder.uri.toString())) { + updatedGitIgnoreSetting = true; + this.ignoreFileResourcesPerRoot.delete(folder.uri.toString()); + this.ignoreTreesPerRoot.delete(folder.uri.toString()); + } if (!shouldFire) { const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString()); @@ -672,13 +712,59 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> { this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) }); }); - if (shouldFire) { + if (shouldFire || updatedGitIgnoreSetting) { this.editorsAffectingFilter.clear(); this._onDidChange.fire(); } } + /** + * Given a .gitignore file resource, processes the resource and adds it to the ignore tree which hides explorer items + * @param root The root folder of the workspace as a string. Used for lookup key for ignore tree and resource list + * @param ignoreFileResource The resource of the .gitignore file + * @param update Whether or not we're updating an existing ignore file. If true it deletes the old entry + */ + private async processIgnoreFile(root: string, ignoreFileResource: URI, update?: boolean) { + // Get the name of the directory which the ignore file is in + const dirUri = dirname(ignoreFileResource); + const ignoreTree = this.ignoreTreesPerRoot.get(root); + if (!ignoreTree) { + return; + } + + // Don't process a directory if we already have it in the tree + if (!update && ignoreTree.has(dirUri)) { + return; + } + // Maybe we need a cancellation token here in case it's super long? + const content = await this.fileService.readFile(ignoreFileResource); + + // If it's just an update we update the contents keeping all references the same + if (update) { + const ignoreFile = ignoreTree.get(dirUri); + ignoreFile?.updateContents(content.value.toString()); + } else { + // Otherwise we create a new ignorefile and add it to the tree + const ignoreParent = ignoreTree.findSubstr(dirUri); + const ignoreFile = new IgnoreFile(content.value.toString(), dirUri.path, ignoreParent); + ignoreTree.set(dirUri, ignoreFile); + // If we haven't seen this resource before then we need to add it to the list of resources we're tracking + if (!this.ignoreFileResourcesPerRoot.get(root)?.has(ignoreFileResource)) { + this.ignoreFileResourcesPerRoot.get(root)?.add(ignoreFileResource); + } + } + + // Notify the explorer of the change so we may ignore these files + this._onDidChange.fire(); + } + filter(stat: ExplorerItem, parentVisibility: TreeVisibility): boolean { + // Add newly visited .gitignore files to the ignore tree + if (stat.name === '.gitignore' && this.ignoreTreesPerRoot.has(stat.root.resource.toString())) { + this.processIgnoreFile(stat.root.resource.toString(), stat.resource, false); + return true; + } + return this.isVisible(stat, parentVisibility); } @@ -694,7 +780,13 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> { // Hide those that match Hidden Patterns const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); - if ((cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) || stat.parent?.isExcluded) { + const globMatch = cached?.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name))); + // Small optimization to only traverse gitIgnore if the globMatch from fileExclude returned nothing + const ignoreFile = globMatch ? undefined : this.ignoreTreesPerRoot.get(stat.root.resource.toString())?.findSubstr(stat.resource); + const isIncludedInTraversal = ignoreFile?.isPathIncludedInTraversal(stat.resource.path, stat.isDirectory); + // Doing !undefined returns true and we want it to be false when undefined because that means it's not included in the ignore file + const isIgnoredByIgnoreFile = isIncludedInTraversal === undefined ? false : !isIncludedInTraversal; + if (isIgnoredByIgnoreFile || globMatch || stat.parent?.isExcluded) { stat.isExcluded = true; const editors = this.editorService.visibleEditors; const editor = editors.find(e => e.resource && this.uriIdentityService.extUri.isEqualOrParent(e.resource, stat.resource)); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index fdfcc0a46a0..ebb2b295f0f 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -320,9 +320,7 @@ export class OpenEditorsView extends ViewPane { protected override layoutBody(height: number, width: number): void { super.layoutBody(height, width); - if (this.list) { - this.list.layout(height, width); - } + this.list?.layout(height, width); } private get showGroups(): boolean { @@ -453,7 +451,7 @@ export class OpenEditorsView extends ViewPane { } } - let dirty = this.workingCopyService.dirtyCount; + const dirty = this.workingCopyService.dirtyCount; if (dirty === 0) { this.dirtyCountElement.classList.add('hidden'); } else { @@ -500,8 +498,8 @@ export class OpenEditorsView extends ViewPane { } override getOptimalWidth(): number { - let parentNode = this.list.getHTMLElement(); - let childNodes: HTMLElement[] = [].slice.call(parentNode.querySelectorAll('.open-editor > a')); + const parentNode = this.list.getHTMLElement(); + const childNodes: HTMLElement[] = [].slice.call(parentNode.querySelectorAll('.open-editor > a')); return dom.getLargestChildWidth(parentNode, childNodes); } diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index 4b7b2c6eace..61d0ab50ca7 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -178,17 +178,13 @@ export class ExplorerItem { private updateName(value: string): void { // Re-add to parent since the parent has a name map to children and the name might have changed - if (this._parent) { - this._parent.removeChild(this); - } + this._parent?.removeChild(this); this._name = value; - if (this._parent) { - this._parent.addChild(this); - } + this._parent?.addChild(this); } getId(): string { - return this.resource.toString(); + return this.root.resource.toString() + '::' + this.resource.toString(); } toString(): string { @@ -411,12 +407,8 @@ export class ExplorerItem { * Moves this element under a new parent element. */ move(newParent: ExplorerItem): void { - if (this.nestedParent) { - this.nestedParent.removeChild(this); - } - if (this._parent) { - this._parent.removeChild(this); - } + this.nestedParent?.removeChild(this); + this._parent?.removeChild(this); newParent.removeChild(this); // make sure to remove any previous version of the file if any newParent.addChild(this); this.updateResource(true); diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 5f10b350de4..d903a67b219 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -98,6 +98,7 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb badges: boolean; }; incrementalNaming: 'simple' | 'smart'; + excludeGitIgnore: boolean; fileNesting: { enabled: boolean; expand: boolean; 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 e34b213a567..de846c66d56 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -115,7 +115,7 @@ suite('Files - FileEditorInput', () => { }); test('reports as untitled without supported file scheme', async function () { - let input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingScheme' })); + const input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingScheme' })); assert.ok(input.hasCapability(EditorInputCapabilities.Untitled)); assert.ok(!input.hasCapability(EditorInputCapabilities.Readonly)); @@ -129,7 +129,7 @@ suite('Files - FileEditorInput', () => { const disposable = accessor.fileService.registerProvider('someTestingReadonlyScheme', new ReadonlyInMemoryFileSystemProvider()); try { - let input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingReadonlyScheme' })); + const input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingReadonlyScheme' })); assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); assert.ok(input.hasCapability(EditorInputCapabilities.Readonly)); diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 30fde6dd67c..5a3e059af9e 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -73,8 +73,8 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { let extensions = await this._extensionService.getExtensions(); extensions = extensions.sort((a, b) => { - let boostA = a.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); - let boostB = b.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); + const boostA = a.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); + const boostB = b.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); if (boostA && !boostB) { return -1; diff --git a/src/vs/workbench/contrib/format/browser/formatModified.ts b/src/vs/workbench/contrib/format/browser/formatModified.ts index 787afa34a7c..9ba04eb2b84 100644 --- a/src/vs/workbench/contrib/format/browser/formatModified.ts +++ b/src/vs/workbench/contrib/format/browser/formatModified.ts @@ -69,7 +69,7 @@ export async function getModifiedRanges(accessor: ServicesAccessor, modified: IT if (!isNonEmptyArray(changes)) { return undefined; } - for (let change of changes) { + for (const change of changes) { ranges.push(modified.validateRange(new Range( change.modifiedStartLineNumber, 1, change.modifiedEndLineNumber || change.modifiedStartLineNumber /*endLineNumber is 0 when things got deleted*/, Number.MAX_SAFE_INTEGER) diff --git a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts index 45ab5391361..2766516fdc0 100644 --- a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts +++ b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts @@ -80,7 +80,7 @@ export class InlayHintsAccessibility implements IEditorContribution { const cts = new CancellationTokenSource(); this._sessionDispoosables.add(cts); - for (let hint of hints) { + for (const hint of hints) { await hint.resolve(cts.token); } @@ -116,7 +116,7 @@ export class InlayHintsAccessibility implements IEditorContribution { if (typeof label === 'string') { em.innerText = label; } else { - for (let part of label) { + for (const part of label) { if (part.command) { const link = this._instaService.createInstance(Link, em, { href: asCommandLink(part.command), label: part.label, title: part.command.title }, diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 5559096223e..f108213e01b 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -23,6 +23,7 @@ import { peekViewBorder /*, peekViewEditorBackground, peekViewResultsBackground import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/suggest'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { EditorActivation } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -30,6 +31,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ILogService } from 'vs/platform/log/common/log'; import { Registry } from 'vs/platform/registry/common/platform'; import { contrastBorder, listInactiveSelectionBackground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -48,7 +50,7 @@ import { IInteractiveHistoryService, InteractiveHistoryService } from 'vs/workbe import { NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { CellEditType, CellKind, ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, ICellOutput, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookContentProvider, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; @@ -58,6 +60,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( InteractiveEditor, @@ -242,7 +245,7 @@ class InteractiveInputContentProvider implements ITextModelContentProvider { if (existing) { return existing; } - let result: ITextModel | null = this._modelService.createModel('', null, resource, false); + const result: ITextModel | null = this._modelService.createModel('', null, resource, false); return result; } } @@ -342,6 +345,7 @@ registerAction2(class extends Action2 { const editorGroupService = accessor.get(IEditorGroupsService); const historyService = accessor.get(IInteractiveHistoryService); const kernelService = accessor.get(INotebookKernelService); + const logService = accessor.get(ILogService); const group = columnToEditorGroup(editorGroupService, typeof showOptions === 'number' ? showOptions : showOptions?.viewColumn); const editorOptions = { activation: EditorActivation.PRESERVE, @@ -349,9 +353,11 @@ registerAction2(class extends Action2 { }; if (resource && resource.scheme === Schemas.vscodeInteractive) { + logService.debug('Open interactive window from resource:', resource.toString()); const resourceUri = URI.revive(resource); const editors = editorService.findEditors(resourceUri).filter(id => id.editor instanceof InteractiveEditorInput && id.editor.resource?.toString() === resourceUri.toString()); if (editors.length) { + logService.debug('Find existing interactive window:', resource.toString()); const editorInput = editors[0].editor as InteractiveEditorInput; const currentGroup = editors[0].groupId; const editor = await editorService.openEditor(editorInput, editorOptions, currentGroup); @@ -382,6 +388,8 @@ registerAction2(class extends Action2 { counter++; } while (existingNotebookDocument.has(notebookUri.toString())); + logService.debug('Open new interactive window:', notebookUri.toString(), inputUri.toString()); + if (id) { const allKernels = kernelService.getMatchingKernel({ uri: notebookUri, viewType: 'interactive' }).all; const preferredKernel = allKernels.find(kernel => kernel.id === id); @@ -395,6 +403,7 @@ registerAction2(class extends Action2 { const editorPane = await editorService.openEditor(editorInput, editorOptions, group); const editorControl = editorPane?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; // Extensions must retain references to these URIs to manipulate the interactive editor + logService.debug('New interactive window opened. Notebook editor id', editorControl?.notebookEditor?.getId()); return { notebookUri, inputUri, notebookEditorId: editorControl?.notebookEditor?.getId() }; } }); @@ -711,3 +720,17 @@ registerThemingParticipant((theme) => { // hc: Color.black // }, localize('interactive.inactiveCodeBackground', 'The backgorund color for the current interactive code cell when the editor does not have focus.')); }); + +Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'notebook', + order: 100, + type: 'object', + 'properties': { + [NotebookSetting.interactiveWindowAlwaysScrollOnNewCell]: { + type: 'boolean', + default: true, + markdownDescription: localize('interactiveWindow.alwaysScrollOnNewCell', "Automatically scroll the interactive window to show the output of the last statement executed. If this value is false, the window will only scroll if the last cell was already the one scrolled to.") + }, + } +}); + diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 52fe78d35bb..37d2b4fde50 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -22,7 +22,7 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; 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'; +import { ICellViewModel, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { cellEditorBackground, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; @@ -35,14 +35,13 @@ import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INTERACTIVE_INPUT_CURSOR_BOUNDARY } from 'vs/workbench/contrib/interactive/browser/interactiveCommon'; import { ComplexNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; -import { NotebookCellExecutionState, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IAction } from 'vs/base/common/actions'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; @@ -62,11 +61,6 @@ import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; -const enum ScrollingState { - Initial = 0, - StickyToBottom = 1 -} - const INPUT_CELL_VERTICAL_PADDING = 8; const INPUT_CELL_HORIZONTAL_PADDING_RIGHT = 10; const INPUT_EDITOR_PADDING = 8; @@ -125,7 +119,7 @@ export class InteractiveEditor extends EditorPane { @INotebookKernelService notebookKernelService: INotebookKernelService, @ILanguageService languageService: ILanguageService, @IKeybindingService keybindingService: IKeybindingService, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService private configurationService: IConfigurationService, @IMenuService menuService: IMenuService, @IContextMenuService contextMenuService: IContextMenuService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @@ -154,6 +148,12 @@ export class InteractiveEditor extends EditorPane { codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); this._register(this.#keybindingService.onDidUpdateKeybindings(this.#updateInputDecoration, this)); + this._register(this.#notebookExecutionStateService.onDidChangeCellExecution((e) => { + const cell = this.#notebookWidget.value?.getCellByHandle(e.cellHandle); + if (cell && e.changed?.state) { + this.#scrollIfNecessary(cell); + } + })); } get #inputCellContainerHeight() { @@ -402,6 +402,9 @@ export class InteractiveEditor extends EditorPane { this.#notebookWidget.value!.setOptions({ isReadOnly: true }); + this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidResizeOutput((cvm) => { + this.#scrollIfNecessary(cvm); + })); this.#widgetDisposableStore.add(this.#notebookWidget.value!.onDidFocusWidget(() => this.#onDidFocusWidget.fire())); this.#widgetDisposableStore.add(model.notebook.onDidChangeContent(() => { (model as ComplexNotebookEditorModel).setDirty(false); @@ -458,10 +461,6 @@ export class InteractiveEditor extends EditorPane { } })); - if (this.#notebookWidget.value?.hasModel()) { - this.#registerExecutionScrollListener(this.#notebookWidget.value); - } - const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this.#contextKeyService); if (input.resource && input.historyService.has(input.resource)) { cursorAtBoundaryContext.set('top'); @@ -511,107 +510,26 @@ export class InteractiveEditor extends EditorPane { } } - #lastCell: ICellViewModel | undefined = undefined; - #lastCellDisposable = new DisposableStore(); - #state: ScrollingState = ScrollingState.Initial; - - #cellAtBottom(widget: IActiveNotebookEditorDelegate, cell: ICellViewModel): boolean { - const visibleRanges = widget.visibleRanges; - const cellIndex = widget.getCellIndex(cell); - if (cellIndex === Math.max(...visibleRanges.map(range => range.end))) { + #cellAtBottom(cell: ICellViewModel): boolean { + const visibleRanges = this.#notebookWidget.value?.visibleRanges || []; + const cellIndex = this.#notebookWidget.value?.getCellIndex(cell); + if (cellIndex === Math.max(...visibleRanges.map(range => range.end - 1))) { return true; } return false; } - /** - * - Init state: 0 - * - Will cell insertion: check if the last cell is at the bottom, false, stay 0 - * if true, state 1 (ready for auto reveal) - * - receive a scroll event (scroll even already happened). If the last cell is at bottom, false, 0, true, state 1 - * - height change of the last cell, if state 0, do nothing, if state 1, scroll the last cell fully into view - */ - #registerExecutionScrollListener(widget: NotebookEditorWidget & IActiveNotebookEditorDelegate) { - this.#widgetDisposableStore.add(widget.textModel.onWillAddRemoveCells(e => { - const lastViewCell = widget.cellAt(widget.getLength() - 1); - - // check if the last cell is at the bottom - if (lastViewCell && this.#cellAtBottom(widget, lastViewCell)) { - this.#state = ScrollingState.StickyToBottom; - } else { - this.#state = ScrollingState.Initial; - } - })); - - this.#widgetDisposableStore.add(widget.onDidScroll(() => { - const lastViewCell = widget.cellAt(widget.getLength() - 1); - - // check if the last cell is at the bottom - if (lastViewCell && this.#cellAtBottom(widget, lastViewCell)) { - this.#state = ScrollingState.StickyToBottom; - } else { - this.#state = ScrollingState.Initial; + #scrollIfNecessary(cvm: ICellViewModel) { + const index = this.#notebookWidget.value!.getCellIndex(cvm); + if (index === this.#notebookWidget.value!.getLength() - 1) { + // If we're already at the bottom or auto scroll is enabled, scroll to the bottom + if (this.configurationService.getValue<boolean>(NotebookSetting.interactiveWindowAlwaysScrollOnNewCell) || this.#cellAtBottom(cvm)) { + this.#notebookWidget.value!.scrollToBottom(); } - })); - - this.#widgetDisposableStore.add(widget.textModel.onDidChangeContent(e => { - for (let i = 0; i < e.rawEvents.length; i++) { - const event = e.rawEvents[i]; - - if (event.kind === NotebookCellsChangeType.ModelChange && this.#notebookWidget.value?.hasModel()) { - const lastViewCell = this.#notebookWidget.value.cellAt(this.#notebookWidget.value.getLength() - 1); - if (lastViewCell !== this.#lastCell) { - this.#lastCellDisposable.clear(); - this.#lastCell = lastViewCell; - this.#registerListenerForCell(); - } - } - } - })); - } - - #registerListenerForCell() { - if (!this.#lastCell) { - return; } - - this.#lastCellDisposable.add(this.#lastCell.onDidChangeLayout((e) => { - if (e.totalHeight === undefined) { - // not cell height change - return; - } - - if (!this.#notebookWidget.value) { - return; - } - - if (this.#lastCell instanceof CodeCellViewModel && (e as CodeCellLayoutChangeEvent).outputHeight === undefined && !this.#notebookWidget.value.isScrolledToBottom()) { - return; - } - - if (this.#state !== ScrollingState.StickyToBottom) { - return; - } - - if (this.#lastCell) { - const runState = this.#notebookExecutionStateService.getCellExecution(this.#lastCell.uri)?.state; - if (runState === NotebookCellExecutionState.Executing) { - return; - } - - } - - // scroll to bottom - // postpone to next tick as the list view might not process the output height change yet - // e.g., when we register this listener later than the list view - this.#lastCellDisposable.add(DOM.scheduleAtNextAnimationFrame(() => { - if (this.#state === ScrollingState.StickyToBottom) { - this.#notebookWidget.value!.scrollToBottom(); - } - })); - })); } + #syncWithKernel() { const notebook = this.#notebookWidget.value?.textModel; const textModel = this.#codeEditorWidget.getModel(); @@ -702,7 +620,7 @@ export class InteractiveEditor extends EditorPane { }); } - this.#codeEditorWidget.setDecorations('interactive-decoration', DECORATION_KEY, decorations); + this.#codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); } override focus() { diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 0372c278593..661059e89b9 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -48,12 +48,12 @@ class StoredCounter { constructor(@IStorageService private readonly _storageService: IStorageService, private readonly _key: string) { } get value() { - return this._storageService.getNumber(this._key, StorageScope.GLOBAL, 0); + return this._storageService.getNumber(this._key, StorageScope.PROFILE, 0); } increment(): number { const n = this.value + 1; - this._storageService.store(this._key, n, StorageScope.GLOBAL, StorageTarget.MACHINE); + this._storageService.store(this._key, n, StorageScope.PROFILE, StorageTarget.MACHINE); return n; } } @@ -117,7 +117,7 @@ class EditorStatusContribution implements IWorkbenchContribution { } private _restoreState(): void { - const raw = this._storageService.get(EditorStatusContribution._keyDedicatedItems, StorageScope.GLOBAL, '[]'); + const raw = this._storageService.get(EditorStatusContribution._keyDedicatedItems, StorageScope.PROFILE, '[]'); try { const ids = <string[]>JSON.parse(raw); this._dedicated = new Set(ids); @@ -128,10 +128,10 @@ class EditorStatusContribution implements IWorkbenchContribution { private _storeState(): void { if (this._dedicated.size === 0) { - this._storageService.remove(EditorStatusContribution._keyDedicatedItems, StorageScope.GLOBAL); + this._storageService.remove(EditorStatusContribution._keyDedicatedItems, StorageScope.PROFILE); } else { const raw = JSON.stringify(Array.from(this._dedicated.keys())); - this._storageService.store(EditorStatusContribution._keyDedicatedItems, raw, StorageScope.GLOBAL, StorageTarget.USER); + this._storageService.store(EditorStatusContribution._keyDedicatedItems, raw, StorageScope.PROFILE, StorageTarget.USER); } } @@ -144,7 +144,7 @@ class EditorStatusContribution implements IWorkbenchContribution { const all = this._languageStatusService.getLanguageStatus(editor.getModel()); const combined: ILanguageStatus[] = []; const dedicated: ILanguageStatus[] = []; - for (let item of all) { + for (const item of all) { if (this._dedicated.has(item.id)) { dedicated.push(item); } @@ -344,7 +344,7 @@ class EditorStatusContribution implements IWorkbenchContribution { } private _renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void { - for (let node of parseLinkedText(text).nodes) { + for (const node of parseLinkedText(text).nodes) { if (typeof node === 'string') { const parts = renderLabelWithIcons(node); dom.append(target, ...parts); @@ -405,6 +405,6 @@ registerAction2(class extends Action2 { } run(accessor: ServicesAccessor): void { - accessor.get(IStorageService).remove('languageStatus.interactCount', StorageScope.GLOBAL); + accessor.get(IStorageService).remove('languageStatus.interactCount', StorageScope.PROFILE); } }); diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts index 49a5a67e8e7..064b4475a96 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts @@ -7,6 +7,8 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { Schemas } from 'vs/base/common/network'; +import Severity from 'vs/base/common/severity'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; @@ -274,7 +276,19 @@ async function restore(accessor: ServicesAccessor, item: ITimelineCommandArgumen } // Replace target with contents of history entry - await fileService.cloneFile(entry.location, entry.workingCopy.resource); + try { + await fileService.cloneFile(entry.location, entry.workingCopy.resource); + } catch (error) { + + // It is possible that we fail to copy the history entry to the + // destination, for example when the destination is write protected. + // In that case tell the user and return, it is still possible for + // the user to manually copy the changes over from the diff editor. + + await dialogService.show(Severity.Error, localize('unableToRestore', "Unable to restore '{0}'.", basename(entry.workingCopy.resource)), undefined, { detail: toErrorMessage(error) }); + + return; + } // Restore all working copies for target if (workingCopies) { @@ -540,7 +554,7 @@ registerAction2(class extends Action2 { inputBox.placeholder = localize('createLocalHistoryPlaceholder', "Enter the new name of the local history entry for '{0}'", labelService.getUriBasenameLabel(resource)); inputBox.show(); inputBox.onDidAccept(async () => { - let entrySource = inputBox.value; + const entrySource = inputBox.value; inputBox.dispose(); if (entrySource) { diff --git a/src/vs/workbench/contrib/localization/browser/localeService.ts b/src/vs/workbench/contrib/localization/browser/localeService.ts new file mode 100644 index 00000000000..c59d84821b2 --- /dev/null +++ b/src/vs/workbench/contrib/localization/browser/localeService.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { language } from 'vs/base/common/platform'; +import { ILanguagePackItem } from 'vs/platform/languagePacks/common/languagePacks'; +import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; + +export class WebLocaleService implements ILocaleService { + declare readonly _serviceBrand: undefined; + + async setLocale(languagePackItem: ILanguagePackItem): Promise<boolean> { + const locale = languagePackItem.id; + if (locale === language || (!locale && language === navigator.language)) { + return false; + } + if (locale) { + window.localStorage.setItem('vscode.nls.locale', locale); + } else { + window.localStorage.removeItem('vscode.nls.locale'); + } + return true; + } + + async clearLocalePreference(): Promise<boolean> { + if (language === navigator.language) { + return false; + } + window.localStorage.removeItem('vscode.nls.locale'); + return true; + } +} diff --git a/src/vs/workbench/contrib/localization/browser/localization.contribution.ts b/src/vs/workbench/contrib/localization/browser/localization.contribution.ts new file mode 100644 index 00000000000..85716e8bcb9 --- /dev/null +++ b/src/vs/workbench/contrib/localization/browser/localization.contribution.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { WebLocaleService } from 'vs/workbench/contrib/localization/browser/localeService'; +import { ClearDisplayLanguageAction, ConfigureDisplayLanguageAction } from 'vs/workbench/contrib/localization/browser/localizationsActions'; +import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; + +registerSingleton(ILocaleService, WebLocaleService, true); + +// Register action to configure locale and related settings +registerAction2(ConfigureDisplayLanguageAction); +registerAction2(ClearDisplayLanguageAction); diff --git a/src/vs/workbench/contrib/localization/browser/localizationsActions.ts b/src/vs/workbench/contrib/localization/browser/localizationsActions.ts new file mode 100644 index 00000000000..5bdb7500724 --- /dev/null +++ b/src/vs/workbench/contrib/localization/browser/localizationsActions.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILanguagePackItem, ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; +import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; + +const restart = localize('restart', "&&Restart"); + +export class ConfigureDisplayLanguageAction extends Action2 { + public static readonly ID = 'workbench.action.configureLocale'; + public static readonly LABEL = localize('configureLocale', "Configure Display Language"); + + constructor() { + super({ + id: ConfigureDisplayLanguageAction.ID, + title: { original: 'Configure Display Language', value: ConfigureDisplayLanguageAction.LABEL }, + menu: { + id: MenuId.CommandPalette + } + }); + } + + public async run(accessor: ServicesAccessor): Promise<void> { + const languagePackService: ILanguagePackService = accessor.get(ILanguagePackService); + const quickInputService: IQuickInputService = accessor.get(IQuickInputService); + const hostService: IHostService = accessor.get(IHostService); + const dialogService: IDialogService = accessor.get(IDialogService); + const productService: IProductService = accessor.get(IProductService); + const localeService: ILocaleService = accessor.get(ILocaleService); + + const installedLanguages = await languagePackService.getInstalledLanguages(); + + const qp = quickInputService.createQuickPick<ILanguagePackItem>(); + qp.placeholder = localize('chooseLocale', "Select Display Language"); + + if (installedLanguages?.length) { + const items: Array<ILanguagePackItem | IQuickPickSeparator> = [{ type: 'separator', label: localize('installed', "Installed") }]; + qp.items = items.concat(installedLanguages); + } + + const disposables = new DisposableStore(); + const source = new CancellationTokenSource(); + disposables.add(qp.onDispose(() => { + source.cancel(); + disposables.dispose(); + })); + + const installedSet = new Set<string>(installedLanguages?.map(language => language.id!) ?? []); + languagePackService.getAvailableLanguages().then(availableLanguages => { + const newLanguages = availableLanguages.filter(l => l.id && !installedSet.has(l.id)); + if (newLanguages.length) { + qp.items = [ + ...qp.items, + { type: 'separator', label: localize('available', "Available") }, + ...newLanguages + ]; + } + qp.busy = false; + }); + + disposables.add(qp.onDidAccept(async () => { + const selectedLanguage = qp.activeItems[0]; + qp.hide(); + + if (await localeService.setLocale(selectedLanguage)) { + const restartDialog = await dialogService.confirm({ + type: 'info', + message: localize('relaunchDisplayLanguageMessage', "A restart is required for the change in display language to take effect."), + detail: localize('relaunchDisplayLanguageDetail', "Press the restart button to restart {0} and change the display language.", productService.nameLong), + primaryButton: restart + }); + + if (restartDialog.confirmed) { + hostService.restart(); + } + } + })); + + qp.show(); + qp.busy = true; + } +} + +export class ClearDisplayLanguageAction extends Action2 { + public static readonly ID = 'workbench.action.clearLocalePreference'; + public static readonly LABEL = localize('clearDisplayLanguage', "Clear Display Language Preference"); + + constructor() { + super({ + id: ClearDisplayLanguageAction.ID, + title: { original: 'Clear Display Language Preference', value: ClearDisplayLanguageAction.LABEL }, + menu: { + id: MenuId.CommandPalette + } + }); + } + + public async run(accessor: ServicesAccessor): Promise<void> { + const localeService: ILocaleService = accessor.get(ILocaleService); + const dialogService: IDialogService = accessor.get(IDialogService); + const productService: IProductService = accessor.get(IProductService); + const hostService: IHostService = accessor.get(IHostService); + + if (await localeService.clearLocalePreference()) { + const restartDialog = await dialogService.confirm({ + type: 'info', + message: localize('relaunchAfterClearDisplayLanguageMessage', "A restart is required for the change in display language to take effect."), + detail: localize('relaunchAfterClearDisplayLanguageDetail', "Press the restart button to restart {0} and change the display language.", productService.nameLong), + primaryButton: restart + }); + + if (restartDialog.confirmed) { + hostService.restart(); + } + } + } +} diff --git a/src/vs/workbench/contrib/localization/common/locale.ts b/src/vs/workbench/contrib/localization/common/locale.ts new file mode 100644 index 00000000000..f447d40bc41 --- /dev/null +++ b/src/vs/workbench/contrib/localization/common/locale.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILanguagePackItem } from 'vs/platform/languagePacks/common/languagePacks'; + +export const ILocaleService = createDecorator<ILocaleService>('localizationService'); + +export interface ILocaleService { + readonly _serviceBrand: undefined; + setLocale(languagePackItem: ILanguagePackItem): Promise<boolean>; + clearLocalePreference(): Promise<boolean>; +} diff --git a/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts b/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts new file mode 100644 index 00000000000..00a6ec7036f --- /dev/null +++ b/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { language } from 'vs/base/common/platform'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; +import { ILanguagePackItem, ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { localize } from 'vs/nls'; +import { toAction } from 'vs/base/common/actions'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { stripComments } from 'vs/base/common/stripComments'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class NativeLocaleService implements ILocaleService { + _serviceBrand: undefined; + + constructor( + @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @INotificationService private readonly notificationService: INotificationService, + @ILanguagePackService private readonly languagePackService: ILanguagePackService, + @IPaneCompositePartService private readonly paneCompositePartService: IPaneCompositePartService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IProgressService private readonly progressService: IProgressService, + @ITextFileService private readonly textFileService: ITextFileService, + @IEditorService private readonly editorService: IEditorService + ) { } + + private async validateLocaleFile(): Promise<boolean> { + try { + const content = await this.textFileService.read(this.environmentService.argvResource, { encoding: 'utf8' }); + + // This is the same logic that we do where argv.json is parsed so mirror that: + // https://github.com/microsoft/vscode/blob/32d40cf44e893e87ac33ac4f08de1e5f7fe077fc/src/main.js#L238-L246 + JSON.parse(stripComments(content.value)); + } catch (error) { + this.notificationService.notify({ + severity: Severity.Error, + message: localize('argvInvalid', 'Unable to write display language. Please open the runtime settings, correct errors/warnings in it and try again.'), + actions: { + primary: [ + toAction({ + id: 'openArgv', + label: localize('openArgv', "Open Runtime Settings"), + run: () => this.editorService.openEditor({ resource: this.environmentService.argvResource }) + }) + ] + } + }); + return false; + } + return true; + } + + private async writeLocaleValue(locale: string | undefined): Promise<boolean> { + if (!(await this.validateLocaleFile())) { + return false; + } + await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true); + return true; + } + + async setLocale(languagePackItem: ILanguagePackItem): Promise<boolean> { + const locale = languagePackItem.id; + if (locale === language || (!locale && language === 'en')) { + return false; + } + const installedLanguages = await this.languagePackService.getInstalledLanguages(); + try { + + // Only Desktop has the concept of installing language packs so we only do this for Desktop + // and only if the language pack is not installed + if (!installedLanguages.some(installedLanguage => installedLanguage.id === languagePackItem.id)) { + + // Only actually install a language pack from Microsoft + if (languagePackItem.galleryExtension?.publisher.toLowerCase() !== 'ms-ceintl') { + // Show the view so the user can see the language pack that they should install + // as of now, there are no 3rd party language packs available on the Marketplace. + const viewlet = await this.paneCompositePartService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar); + (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(`@id:${languagePackItem.extensionId}`); + return false; + } + + await this.progressService.withProgress( + { + location: ProgressLocation.Notification, + title: localize('installing', "Installing {0} language support...", languagePackItem.label), + }, + progress => this.extensionManagementService.installFromGallery(languagePackItem.galleryExtension!, { + // Setting this to false is how you get the extension to be synced with Settings Sync (if enabled). + isMachineScoped: false, + }) + ); + } + + return await this.writeLocaleValue(locale); + } catch (err) { + this.notificationService.error(err); + return false; + } + } + + async clearLocalePreference(): Promise<boolean> { + if (language === 'en') { + return false; + } + try { + return await this.writeLocaleValue(undefined); + } catch (err) { + this.notificationService.error(err); + return false; + } + } +} diff --git a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts index 92c5e9c0348..b993ec9f13d 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizations.contribution.ts +++ b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts @@ -6,30 +6,34 @@ import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution, Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ConfigureLocaleAction } from 'vs/workbench/contrib/localizations/browser/localizationsActions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { IExtensionManagementService, IExtensionGalleryService, IGalleryExtension, InstallOperation, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotificationService, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; import Severity from 'vs/base/common/severity'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { VIEWLET_ID as EXTENSIONS_VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; -import { minimumTranslatedStrings } from 'vs/workbench/contrib/localizations/browser/minimalTranslations'; +import { minimumTranslatedStrings } from 'vs/workbench/contrib/localization/electron-sandbox/minimalTranslations'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { ClearDisplayLanguageAction, ConfigureDisplayLanguageAction } from 'vs/workbench/contrib/localization/browser/localizationsActions'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; +import { NativeLocaleService } from 'vs/workbench/contrib/localization/electron-sandbox/localeService'; + +registerSingleton(ILocaleService, NativeLocaleService, true); // Register action to configure locale and related settings -const registry = Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureLocaleAction), 'Configure Display Language'); +registerAction2(ConfigureDisplayLanguageAction); +registerAction2(ClearDisplayLanguageAction); const LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY = 'extensionsAssistant/languagePackSuggestionIgnore'; @@ -73,7 +77,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo }], { sticky: true, - neverShowAgain: { id: 'langugage.update.donotask', isSecondary: true } + neverShowAgain: { id: 'langugage.update.donotask', isSecondary: true, scope: NeverShowAgainScope.APPLICATION } } ); } @@ -84,7 +88,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo private checkAndInstall(): void { const language = platform.language; const locale = platform.locale; - const languagePackSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get(LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY, StorageScope.GLOBAL, '[]')); + const languagePackSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get(LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY, StorageScope.APPLICATION, '[]')); if (!this.galleryService.isEnabled()) { return; @@ -119,7 +123,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo const loc = manifest && manifest.contributes && manifest.contributes.localizations && manifest.contributes.localizations.filter(x => x.languageId.toLowerCase() === locale)[0]; const languageName = loc ? (loc.languageName || locale) : locale; const languageDisplayName = loc ? (loc.localizedLanguageName || loc.languageName || locale) : locale; - const translationsFromPack: any = translation && translation.contents ? translation.contents['vs/workbench/contrib/localizations/browser/minimalTranslations'] : {}; + const translationsFromPack: any = translation && translation.contents ? translation.contents['vs/workbench/contrib/localization/electron-sandbox/minimalTranslations'] : {}; const promptMessageKey = extensionToInstall ? 'installAndRestartMessage' : 'showLanguagePackExtensions'; const useEnglish = !translationsFromPack[promptMessageKey]; @@ -135,6 +139,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo const logUserReaction = (userReaction: string) => { /* __GDPR__ "languagePackSuggestion:popup" : { + "owner": "TylerLeonhardt", "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "language": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } @@ -177,7 +182,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo this.storageService.store( LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY, JSON.stringify(languagePackSuggestionIgnoreList), - StorageScope.GLOBAL, + StorageScope.APPLICATION, StorageTarget.USER ); logUserReaction('neverShowAgain'); @@ -196,14 +201,13 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo } - private isLanguageInstalled(language: string | undefined): Promise<boolean> { - return this.extensionManagementService.getInstalled() - .then(installed => installed.some(i => - !!(i.manifest - && i.manifest.contributes - && i.manifest.contributes.localizations - && i.manifest.contributes.localizations.length - && i.manifest.contributes.localizations.some(l => l.languageId.toLowerCase() === language)))); + private async isLanguageInstalled(language: string | undefined): Promise<boolean> { + const installed = await this.extensionManagementService.getInstalled(); + return installed.some(i => !!(i.manifest + && i.manifest.contributes + && i.manifest.contributes.localizations + && i.manifest.contributes.localizations.length + && i.manifest.contributes.localizations.some(l => l.languageId.toLowerCase() === language))); } private installExtension(extension: IGalleryExtension): Promise<void> { diff --git a/src/vs/workbench/contrib/localizations/browser/minimalTranslations.ts b/src/vs/workbench/contrib/localization/electron-sandbox/minimalTranslations.ts index 00cffc61629..00cffc61629 100644 --- a/src/vs/workbench/contrib/localizations/browser/minimalTranslations.ts +++ b/src/vs/workbench/contrib/localization/electron-sandbox/minimalTranslations.ts diff --git a/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts b/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts deleted file mode 100644 index 5384fc3e0b0..00000000000 --- a/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts +++ /dev/null @@ -1,87 +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 { localize } from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { language } from 'vs/base/common/platform'; -import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; - -export class ConfigureLocaleAction extends Action { - public static readonly ID = 'workbench.action.configureLocale'; - public static readonly LABEL = localize('configureLocale', "Configure Display Language"); - - constructor(id: string, label: string, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @ILocalizationsService private readonly localizationService: ILocalizationsService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, - @IHostService private readonly hostService: IHostService, - @INotificationService private readonly notificationService: INotificationService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, - @IDialogService private readonly dialogService: IDialogService, - @IProductService private readonly productService: IProductService - ) { - super(id, label); - } - - private async getLanguageOptions(): Promise<IQuickPickItem[]> { - const availableLanguages = await this.localizationService.getLanguageIds(); - availableLanguages.sort(); - - return availableLanguages - .map(language => { return { label: language }; }) - .concat({ label: localize('installAdditionalLanguages', "Install Additional Languages...") }); - } - - public override async run(): Promise<void> { - const languageOptions = await this.getLanguageOptions(); - const currentLanguageIndex = languageOptions.findIndex(l => l.label === language); - - try { - const selectedLanguage = await this.quickInputService.pick(languageOptions, - { - canPickMany: false, - placeHolder: localize('chooseDisplayLanguage', "Select Display Language"), - activeItem: languageOptions[currentLanguageIndex] - }); - - if (selectedLanguage === languageOptions[languageOptions.length - 1]) { - return this.paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer()) - .then(viewlet => { - const extensionsViewlet = viewlet as IExtensionsViewPaneContainer; - extensionsViewlet.search('@category:"language packs"'); - extensionsViewlet.focus(); - }); - } - - if (selectedLanguage) { - await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: selectedLanguage.label }], true); - const restart = await this.dialogService.confirm({ - type: 'info', - message: localize('relaunchDisplayLanguageMessage', "A restart is required for the change in display language to take effect."), - detail: localize('relaunchDisplayLanguageDetail', "Press the restart button to restart {0} and change the display language.", this.productService.nameLong), - primaryButton: localize('restart', "&&Restart") - }); - - if (restart.confirmed) { - this.hostService.restart(); - } - } - } catch (e) { - this.notificationService.error(e); - } - } -} diff --git a/src/vs/workbench/contrib/markers/browser/constants.ts b/src/vs/workbench/contrib/markers/browser/constants.ts deleted file mode 100644 index 82f1acb1779..00000000000 --- a/src/vs/workbench/contrib/markers/browser/constants.ts +++ /dev/null @@ -1,31 +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 { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; - -export default { - MARKERS_CONTAINER_ID: 'workbench.panel.markers', - MARKERS_VIEW_ID: 'workbench.panel.markers.view', - MARKERS_VIEW_STORAGE_ID: 'workbench.panel.markers', - MARKER_COPY_ACTION_ID: 'problems.action.copy', - MARKER_COPY_MESSAGE_ACTION_ID: 'problems.action.copyMessage', - RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID: 'problems.action.copyRelatedInformationMessage', - FOCUS_PROBLEMS_FROM_FILTER: 'problems.action.focusProblemsFromFilter', - MARKERS_VIEW_FOCUS_FILTER: 'problems.action.focusFilter', - MARKERS_VIEW_CLEAR_FILTER_TEXT: 'problems.action.clearFilterText', - MARKERS_VIEW_SHOW_MULTILINE_MESSAGE: 'problems.action.showMultilineMessage', - MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE: 'problems.action.showSinglelineMessage', - MARKER_OPEN_ACTION_ID: 'problems.action.open', - MARKER_OPEN_SIDE_ACTION_ID: 'problems.action.openToSide', - MARKER_SHOW_PANEL_ID: 'workbench.action.showErrorsWarnings', - MARKER_SHOW_QUICK_FIX: 'problems.action.showQuickFixes', - TOGGLE_MARKERS_VIEW_ACTION_ID: 'workbench.actions.view.toggleProblems', - - MarkersViewSmallLayoutContextKey: new RawContextKey<boolean>(`problemsView.smallLayout`, false), - MarkersTreeVisibilityContextKey: new RawContextKey<boolean>('problemsVisibility', false), - MarkerFocusContextKey: new RawContextKey<boolean>('problemFocus', false), - MarkerViewFilterFocusContextKey: new RawContextKey<boolean>('problemsFilterFocus', false), - RelatedInformationFocusContextKey: new RawContextKey<boolean>('relatedInformationFocus', false) -}; diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index b17ef050f94..66e8b574bc6 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -14,13 +14,13 @@ import { Marker, RelatedInformation, ResourceMarkers } from 'vs/workbench/contri import { MarkersView } from 'vs/workbench/contrib/markers/browser/markersView'; import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; -import Constants from 'vs/workbench/contrib/markers/browser/constants'; +import { MarkersViewMode, Markers, MarkersContextKeys } from 'vs/workbench/contrib/markers/common/markers'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { ActivityUpdater, IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; +import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IMarkerService, MarkerStatistics } from 'vs/platform/markers/common/markers'; import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService } from 'vs/workbench/common/views'; @@ -31,53 +31,54 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; +import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.MARKER_OPEN_ACTION_ID, + id: Markers.MARKER_OPEN_ACTION_ID, weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.MarkerFocusContextKey), + when: ContextKeyExpr.and(MarkersContextKeys.MarkerFocusContextKey), primary: KeyCode.Enter, mac: { primary: KeyCode.Enter, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] }, handler: (accessor, args: any) => { - const markersView = accessor.get(IViewsService).getActiveViewWithId<MarkersView>(Constants.MARKERS_VIEW_ID)!; + const markersView = accessor.get(IViewsService).getActiveViewWithId<MarkersView>(Markers.MARKERS_VIEW_ID)!; markersView.openFileAtElement(markersView.getFocusElement(), false, false, true); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.MARKER_OPEN_SIDE_ACTION_ID, + id: Markers.MARKER_OPEN_SIDE_ACTION_ID, weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.MarkerFocusContextKey), + when: ContextKeyExpr.and(MarkersContextKeys.MarkerFocusContextKey), primary: KeyMod.CtrlCmd | KeyCode.Enter, mac: { primary: KeyMod.WinCtrl | KeyCode.Enter }, handler: (accessor, args: any) => { - const markersView = accessor.get(IViewsService).getActiveViewWithId<MarkersView>(Constants.MARKERS_VIEW_ID)!; + const markersView = accessor.get(IViewsService).getActiveViewWithId<MarkersView>(Markers.MARKERS_VIEW_ID)!; markersView.openFileAtElement(markersView.getFocusElement(), false, true, true); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.MARKER_SHOW_PANEL_ID, + id: Markers.MARKER_SHOW_PANEL_ID, weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, handler: async (accessor, args: any) => { - await accessor.get(IViewsService).openView(Constants.MARKERS_VIEW_ID); + await accessor.get(IViewsService).openView(Markers.MARKERS_VIEW_ID); } }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.MARKER_SHOW_QUICK_FIX, + id: Markers.MARKER_SHOW_QUICK_FIX, weight: KeybindingWeight.WorkbenchContrib, - when: Constants.MarkerFocusContextKey, + when: MarkersContextKeys.MarkerFocusContextKey, primary: KeyMod.CtrlCmd | KeyCode.Period, handler: (accessor, args: any) => { - const markersView = accessor.get(IViewsService).getActiveViewWithId<MarkersView>(Constants.MARKERS_VIEW_ID)!; + const markersView = accessor.get(IViewsService).getActiveViewWithId<MarkersView>(Markers.MARKERS_VIEW_ID)!; const focusedElement = markersView.getFocusElement(); if (focusedElement instanceof Marker) { markersView.showQuickFixes(focusedElement); @@ -97,6 +98,12 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurat 'type': 'boolean', 'default': true }, + 'problems.defaultViewMode': { + 'description': Messages.PROBLEMS_PANEL_CONFIGURATION_VIEW_MODE, + 'type': 'string', + 'default': 'tree', + 'enum': ['table', 'tree'], + }, 'problems.showCurrentInStatus': { 'description': Messages.PROBLEMS_PANEL_CONFIGURATION_SHOW_CURRENT_STATUS, 'type': 'boolean', @@ -119,17 +126,17 @@ const markersViewIcon = registerIcon('markers-view-icon', Codicon.warning, local // markers view container const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ - id: Constants.MARKERS_CONTAINER_ID, + id: Markers.MARKERS_CONTAINER_ID, title: Messages.MARKERS_PANEL_TITLE_PROBLEMS, icon: markersViewIcon, hideIfEmpty: true, order: 0, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), - storageId: Constants.MARKERS_VIEW_STORAGE_ID, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Markers.MARKERS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + storageId: Markers.MARKERS_VIEW_STORAGE_ID, }, ViewContainerLocation.Panel, { donotRegisterOpenCommand: true }); Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry).registerViews([{ - id: Constants.MARKERS_VIEW_ID, + id: Markers.MARKERS_VIEW_ID, containerIcon: markersViewIcon, name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, canToggleVisibility: false, @@ -145,9 +152,50 @@ Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry).registerViews // workbench const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench); -workbenchRegistry.registerWorkbenchContribution(ActivityUpdater, LifecyclePhase.Restored); // actions +registerAction2(class extends ViewAction<IMarkersView> { + constructor() { + super({ + id: `workbench.actions.table.${Markers.MARKERS_VIEW_ID}.viewAsTree`, + title: localize('viewAsTree', "View as Tree"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID), MarkersContextKeys.MarkersViewModeContextKey.isEqualTo(MarkersViewMode.Table)), + group: 'navigation', + order: 3 + }, + icon: Codicon.listTree, + viewId: Markers.MARKERS_VIEW_ID + }); + } + + async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise<void> { + view.setViewMode(MarkersViewMode.Tree); + } +}); + +registerAction2(class extends ViewAction<IMarkersView> { + constructor() { + super({ + id: `workbench.actions.table.${Markers.MARKERS_VIEW_ID}.viewAsTable`, + title: localize('viewAsTable', "View as Table"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID), MarkersContextKeys.MarkersViewModeContextKey.isEqualTo(MarkersViewMode.Tree)), + group: 'navigation', + order: 3 + }, + icon: Codicon.listFlat, + viewId: Markers.MARKERS_VIEW_ID + }); + } + + async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise<void> { + view.setViewMode(MarkersViewMode.Table); + } +}); + registerAction2(class extends Action2 { constructor() { super({ @@ -158,15 +206,15 @@ registerAction2(class extends Action2 { }); } async run(accessor: ServicesAccessor): Promise<void> { - accessor.get(IViewsService).openView(Constants.MARKERS_VIEW_ID, true); + accessor.get(IViewsService).openView(Markers.MARKERS_VIEW_ID, true); } }); registerAction2(class extends ViewAction<IMarkersView> { constructor() { - const when = ContextKeyExpr.and(FocusedViewContext.isEqualTo(Constants.MARKERS_VIEW_ID), Constants.MarkersTreeVisibilityContextKey, Constants.RelatedInformationFocusContextKey.toNegated()); + const when = ContextKeyExpr.and(FocusedViewContext.isEqualTo(Markers.MARKERS_VIEW_ID), MarkersContextKeys.MarkersTreeVisibilityContextKey, MarkersContextKeys.RelatedInformationFocusContextKey.toNegated()); super({ - id: Constants.MARKER_COPY_ACTION_ID, + id: Markers.MARKER_COPY_ACTION_ID, title: { value: localize('copyMarker', "Copy"), original: 'Copy' }, menu: { id: MenuId.ProblemsPanelContext, @@ -178,7 +226,7 @@ registerAction2(class extends ViewAction<IMarkersView> { primary: KeyMod.CtrlCmd | KeyCode.KeyC, when }, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise<void> { @@ -206,14 +254,14 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends ViewAction<IMarkersView> { constructor() { super({ - id: Constants.MARKER_COPY_MESSAGE_ACTION_ID, + id: Markers.MARKER_COPY_MESSAGE_ACTION_ID, title: { value: localize('copyMessage', "Copy Message"), original: 'Copy Message' }, menu: { id: MenuId.ProblemsPanelContext, - when: Constants.MarkerFocusContextKey, + when: MarkersContextKeys.MarkerFocusContextKey, group: 'navigation' }, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise<void> { @@ -228,14 +276,14 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends ViewAction<IMarkersView> { constructor() { super({ - id: Constants.RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID, + id: Markers.RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID, title: { value: localize('copyMessage', "Copy Message"), original: 'Copy Message' }, menu: { id: MenuId.ProblemsPanelContext, - when: Constants.RelatedInformationFocusContextKey, + when: MarkersContextKeys.RelatedInformationFocusContextKey, group: 'navigation' }, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise<void> { @@ -250,14 +298,14 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends ViewAction<IMarkersView> { constructor() { super({ - id: Constants.FOCUS_PROBLEMS_FROM_FILTER, + id: Markers.FOCUS_PROBLEMS_FROM_FILTER, title: localize('focusProblemsList', "Focus problems view"), keybinding: { - when: Constants.MarkerViewFilterFocusContextKey, + when: MarkersContextKeys.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.DownArrow }, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise<void> { @@ -268,14 +316,14 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends ViewAction<IMarkersView> { constructor() { super({ - id: Constants.MARKERS_VIEW_FOCUS_FILTER, + id: Markers.MARKERS_VIEW_FOCUS_FILTER, title: localize('focusProblemsFilter', "Focus problems filter"), keybinding: { - when: FocusedViewContext.isEqualTo(Constants.MARKERS_VIEW_ID), + when: FocusedViewContext.isEqualTo(Markers.MARKERS_VIEW_ID), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyF }, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise<void> { @@ -286,14 +334,14 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends ViewAction<IMarkersView> { constructor() { super({ - id: Constants.MARKERS_VIEW_SHOW_MULTILINE_MESSAGE, + id: Markers.MARKERS_VIEW_SHOW_MULTILINE_MESSAGE, title: { value: localize('show multiline', "Show message in multiple lines"), original: 'Problems: Show message in multiple lines' }, category: localize('problems', "Problems"), menu: { id: MenuId.CommandPalette, - when: ContextKeyExpr.has(getVisbileViewContextKey(Constants.MARKERS_VIEW_ID)) + when: ContextKeyExpr.has(getVisbileViewContextKey(Markers.MARKERS_VIEW_ID)) }, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise<void> { @@ -304,14 +352,14 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends ViewAction<IMarkersView> { constructor() { super({ - id: Constants.MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE, + id: Markers.MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE, title: { value: localize('show singleline', "Show message in single line"), original: 'Problems: Show message in single line' }, category: localize('problems', "Problems"), menu: { id: MenuId.CommandPalette, - when: ContextKeyExpr.has(getVisbileViewContextKey(Constants.MARKERS_VIEW_ID)) + when: ContextKeyExpr.has(getVisbileViewContextKey(Markers.MARKERS_VIEW_ID)) }, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise<void> { @@ -322,15 +370,15 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends ViewAction<IMarkersView> { constructor() { super({ - id: Constants.MARKERS_VIEW_CLEAR_FILTER_TEXT, + id: Markers.MARKERS_VIEW_CLEAR_FILTER_TEXT, title: localize('clearFiltersText', "Clear filters text"), category: localize('problems', "Problems"), keybinding: { - when: Constants.MarkerViewFilterFocusContextKey, + when: MarkersContextKeys.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape }, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, markersView: IMarkersView): Promise<void> { @@ -341,16 +389,16 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends ViewAction<IMarkersView> { constructor() { super({ - id: `workbench.actions.treeView.${Constants.MARKERS_VIEW_ID}.collapseAll`, + id: `workbench.actions.treeView.${Markers.MARKERS_VIEW_ID}.collapseAll`, title: localize('collapseAll', "Collapse All"), menu: { id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', Constants.MARKERS_VIEW_ID), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID), MarkersContextKeys.MarkersViewModeContextKey.isEqualTo(MarkersViewMode.Tree)), group: 'navigation', order: 2, }, icon: Codicon.collapseAll, - viewId: Constants.MARKERS_VIEW_ID + viewId: Markers.MARKERS_VIEW_ID }); } async runInView(serviceAccessor: ServicesAccessor, view: IMarkersView): Promise<void> { @@ -361,11 +409,11 @@ registerAction2(class extends ViewAction<IMarkersView> { registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.actions.treeView.${Constants.MARKERS_VIEW_ID}.filter`, + id: `workbench.actions.treeView.${Markers.MARKERS_VIEW_ID}.filter`, title: localize('filter', "Filter"), menu: { id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', Constants.MARKERS_VIEW_ID), Constants.MarkersViewSmallLayoutContextKey.negate()), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', Markers.MARKERS_VIEW_ID), MarkersContextKeys.MarkersViewSmallLayoutContextKey.negate()), group: 'navigation', order: 1, }, @@ -377,16 +425,16 @@ registerAction2(class extends Action2 { registerAction2(class extends Action2 { constructor() { super({ - id: Constants.TOGGLE_MARKERS_VIEW_ACTION_ID, + id: Markers.TOGGLE_MARKERS_VIEW_ACTION_ID, title: Messages.MARKERS_PANEL_TOGGLE_LABEL, }); } async run(accessor: ServicesAccessor): Promise<void> { const viewsService = accessor.get(IViewsService); - if (viewsService.isViewVisible(Constants.MARKERS_VIEW_ID)) { - viewsService.closeView(Constants.MARKERS_VIEW_ID); + if (viewsService.isViewVisible(Markers.MARKERS_VIEW_ID)) { + viewsService.closeView(Markers.MARKERS_VIEW_ID); } else { - viewsService.openView(Constants.MARKERS_VIEW_ID, true); + viewsService.openView(Markers.MARKERS_VIEW_ID, true); } } }); @@ -466,3 +514,26 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont } workbenchRegistry.registerWorkbenchContribution(MarkersStatusBarContributions, LifecyclePhase.Restored); + +class ActivityUpdater extends Disposable implements IWorkbenchContribution { + + private readonly activity = this._register(new MutableDisposable<IDisposable>()); + + constructor( + @IActivityService private readonly activityService: IActivityService, + @IMarkerService private readonly markerService: IMarkerService + ) { + super(); + this._register(this.markerService.onMarkerChanged(() => this.updateBadge())); + this.updateBadge(); + } + + private updateBadge(): void { + const { errors, warnings, infos } = this.markerService.getStatistics(); + const total = errors + warnings + infos; + const message = localize('totalProblems', 'Total {0} Problems', total); + this.activity.value = this.activityService.showViewActivity(Markers.MARKERS_VIEW_ID, { badge: new NumberBadge(total, () => message) }); + } +} + +workbenchRegistry.registerWorkbenchContribution(ActivityUpdater, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/markers/browser/markers.ts b/src/vs/workbench/contrib/markers/browser/markers.ts index a62b4a553a4..0969177554d 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.ts @@ -3,16 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IMarkerService } from 'vs/platform/markers/common/markers'; -import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { localize } from 'vs/nls'; -import Constants from './constants'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { MarkersFilters } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { Event } from 'vs/base/common/event'; import { IView } from 'vs/workbench/common/views'; import { MarkerElement, ResourceMarkers } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { MarkersViewMode } from 'vs/workbench/contrib/markers/common/markers'; export interface IMarkersView extends IView { @@ -30,25 +25,5 @@ export interface IMarkersView extends IView { collapseAll(): void; setMultiline(multiline: boolean): void; -} - -export class ActivityUpdater extends Disposable implements IWorkbenchContribution { - - private readonly activity = this._register(new MutableDisposable<IDisposable>()); - - constructor( - @IActivityService private readonly activityService: IActivityService, - @IMarkerService private readonly markerService: IMarkerService - ) { - super(); - this._register(this.markerService.onMarkerChanged(() => this.updateBadge())); - this.updateBadge(); - } - - private updateBadge(): void { - const { errors, warnings, infos } = this.markerService.getStatistics(); - const total = errors + warnings + infos; - const message = localize('totalProblems', 'Total {0} Problems', total); - this.activity.value = this.activityService.showViewActivity(Constants.MARKERS_VIEW_ID, { badge: new NumberBadge(total, () => message) }); - } + setViewMode(viewMode: MarkersViewMode): void; } diff --git a/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts b/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts index acd96da794a..8ca2e6bce00 100644 --- a/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts +++ b/src/vs/workbench/contrib/markers/browser/markersFileDecorations.ts @@ -28,7 +28,7 @@ class MarkersDecorationsProvider implements IDecorationsProvider { } provideDecorations(resource: URI): IDecorationData | undefined { - let markers = this._markerService.read({ + const markers = this._markerService.read({ resource, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); @@ -77,7 +77,7 @@ class MarkersFileDecorations implements IWorkbenchContribution { } private _updateEnablement(): void { - let value = this._configurationService.getValue<{ decorations: { enabled: boolean } }>('problems'); + const value = this._configurationService.getValue<{ decorations: { enabled: boolean } }>('problems'); if (value.decorations.enabled === this._enabled) { return; } diff --git a/src/vs/workbench/contrib/markers/browser/markersModel.ts b/src/vs/workbench/contrib/markers/browser/markersModel.ts index c1155aa2391..2920b8bb5f2 100644 --- a/src/vs/workbench/contrib/markers/browser/markersModel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersModel.ts @@ -13,6 +13,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Hasher } from 'vs/base/common/hash'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { splitLines } from 'vs/base/common/strings'; +import { IMatch } from 'vs/base/common/filters'; export type MarkerElement = ResourceMarkers | Marker | RelatedInformation; @@ -21,8 +22,8 @@ export function compareMarkersByUri(a: IMarker, b: IMarker) { } function compareResourceMarkers(a: ResourceMarkers, b: ResourceMarkers): number { - let [firstMarkerOfA] = a.markers; - let [firstMarkerOfB] = b.markers; + const [firstMarkerOfA] = a.markers; + const [firstMarkerOfB] = b.markers; let res = 0; if (firstMarkerOfA && firstMarkerOfB) { res = MarkerSeverity.compare(firstMarkerOfA.marker.severity, firstMarkerOfB.marker.severity); @@ -70,7 +71,7 @@ export class ResourceMarkers { } delete(uri: URI) { - let array = this._markersMap.get(uri); + const array = this._markersMap.get(uri); if (array) { this._total -= array.length; this._cachedMarkers = undefined; @@ -117,6 +118,19 @@ export class Marker { } } +export class MarkerTableItem extends Marker { + constructor( + marker: Marker, + readonly sourceMatches?: IMatch[], + readonly codeMatches?: IMatch[], + readonly messageMatches?: IMatch[], + readonly fileMatches?: IMatch[], + readonly ownerMatches?: IMatch[], + ) { + super(marker.id, marker.marker, marker.relatedInformation); + } +} + export class RelatedInformation { constructor( diff --git a/src/vs/workbench/contrib/markers/browser/markersTable.ts b/src/vs/workbench/contrib/markers/browser/markersTable.ts new file mode 100644 index 00000000000..a4938d08d58 --- /dev/null +++ b/src/vs/workbench/contrib/markers/browser/markersTable.ts @@ -0,0 +1,561 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import * as network from 'vs/base/common/network'; +import { Event } from 'vs/base/common/event'; +import { ITableContextMenuEvent, ITableEvent, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IOpenEvent, IWorkbenchTableOptions, WorkbenchTable } from 'vs/platform/list/browser/listService'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { compareMarkersByUri, Marker, MarkerTableItem, ResourceMarkers } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilterOptions'; +import { Link } from 'vs/platform/opener/browser/link'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { MarkersViewModel } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; +import { IAction } from 'vs/base/common/actions'; +import { QuickFixAction, QuickFixActionViewItem } from 'vs/workbench/contrib/markers/browser/markersViewActions'; +import { DomEmitter } from 'vs/base/browser/event'; +import Messages from 'vs/workbench/contrib/markers/browser/messages'; +import { isUndefinedOrNull } from 'vs/base/common/types'; +import { IProblemsWidget } from 'vs/workbench/contrib/markers/browser/markersView'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Range } from 'vs/editor/common/core/range'; + +const $ = DOM.$; + +interface IMarkerIconColumnTemplateData { + readonly icon: HTMLElement; + readonly actionBar: ActionBar; +} + +interface IMarkerCodeColumnTemplateData { + readonly codeColumn: HTMLElement; + readonly sourceLabel: HighlightedLabel; + readonly codeLabel: HighlightedLabel; + readonly codeLink: Link; +} + +interface IMarkerFileColumnTemplateData { + readonly columnElement: HTMLElement; + readonly fileLabel: HighlightedLabel; + readonly positionLabel: HighlightedLabel; +} + + +interface IMarkerHighlightedLabelColumnTemplateData { + readonly columnElement: HTMLElement; + readonly highlightedLabel: HighlightedLabel; +} + +class MarkerSeverityColumnRenderer implements ITableRenderer<MarkerTableItem, IMarkerIconColumnTemplateData>{ + + static readonly TEMPLATE_ID = 'severity'; + + readonly templateId: string = MarkerSeverityColumnRenderer.TEMPLATE_ID; + + constructor( + private readonly markersViewModel: MarkersViewModel, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + renderTemplate(container: HTMLElement): IMarkerIconColumnTemplateData { + const severityColumn = DOM.append(container, $('.severity')); + const icon = DOM.append(severityColumn, $('')); + + const actionBarColumn = DOM.append(container, $('.actions')); + const actionBar = new ActionBar(actionBarColumn, { + actionViewItemProvider: (action: IAction) => action.id === QuickFixAction.ID ? this.instantiationService.createInstance(QuickFixActionViewItem, <QuickFixAction>action) : undefined, + animated: false + }); + + return { actionBar, icon }; + } + + renderElement(element: MarkerTableItem, index: number, templateData: IMarkerIconColumnTemplateData, height: number | undefined): void { + const toggleQuickFix = (enabled?: boolean) => { + if (!isUndefinedOrNull(enabled)) { + const container = DOM.findParentWithClass(templateData.icon, 'monaco-table-td')!; + container.classList.toggle('quickFix', enabled); + } + }; + + templateData.icon.title = MarkerSeverity.toString(element.marker.severity); + templateData.icon.className = `marker-icon codicon ${SeverityIcon.className(MarkerSeverity.toSeverity(element.marker.severity))}`; + + templateData.actionBar.clear(); + const viewModel = this.markersViewModel.getViewModel(element); + if (viewModel) { + const quickFixAction = viewModel.quickFixAction; + templateData.actionBar.push([quickFixAction], { icon: true, label: false }); + toggleQuickFix(viewModel.quickFixAction.enabled); + + quickFixAction.onDidChange(({ enabled }) => toggleQuickFix(enabled)); + quickFixAction.onShowQuickFixes(() => { + const quickFixActionViewItem = <QuickFixActionViewItem>templateData.actionBar.viewItems[0]; + if (quickFixActionViewItem) { + quickFixActionViewItem.showQuickFixes(); + } + }); + } + } + + disposeTemplate(templateData: IMarkerIconColumnTemplateData): void { } +} + +class MarkerCodeColumnRenderer implements ITableRenderer<MarkerTableItem, IMarkerCodeColumnTemplateData> { + static readonly TEMPLATE_ID = 'code'; + + readonly templateId: string = MarkerCodeColumnRenderer.TEMPLATE_ID; + + constructor( + @IOpenerService private readonly openerService: IOpenerService + ) { } + + renderTemplate(container: HTMLElement): IMarkerCodeColumnTemplateData { + const codeColumn = DOM.append(container, $('.code')); + + const sourceLabel = new HighlightedLabel(codeColumn); + sourceLabel.element.classList.add('source-label'); + + const codeLabel = new HighlightedLabel(codeColumn); + codeLabel.element.classList.add('code-label'); + + const codeLink = new Link(codeColumn, { href: '', label: '' }, {}, this.openerService); + + return { codeColumn, sourceLabel, codeLabel, codeLink }; + } + + renderElement(element: MarkerTableItem, index: number, templateData: IMarkerCodeColumnTemplateData, height: number | undefined): void { + if (element.marker.source && element.marker.code) { + templateData.codeColumn.classList.toggle('code-link', typeof element.marker.code !== 'string'); + DOM.show(templateData.codeLabel.element); + + if (typeof element.marker.code === 'string') { + templateData.codeColumn.title = `${element.marker.source} (${element.marker.code})`; + templateData.sourceLabel.set(element.marker.source, element.sourceMatches); + templateData.codeLabel.set(element.marker.code, element.codeMatches); + } else { + templateData.codeColumn.title = `${element.marker.source} (${element.marker.code.value})`; + templateData.sourceLabel.set(element.marker.source, element.sourceMatches); + + const codeLinkLabel = new HighlightedLabel($('.code-link-label')); + codeLinkLabel.set(element.marker.code.value, element.codeMatches); + + templateData.codeLink.link = { + href: element.marker.code.target.toString(), + title: element.marker.code.target.toString(), + label: codeLinkLabel.element, + }; + } + } else { + templateData.codeColumn.title = ''; + templateData.sourceLabel.set('-'); + DOM.hide(templateData.codeLabel.element); + } + } + + disposeTemplate(templateData: IMarkerCodeColumnTemplateData): void { } +} + +class MarkerMessageColumnRenderer implements ITableRenderer<MarkerTableItem, IMarkerHighlightedLabelColumnTemplateData>{ + + static readonly TEMPLATE_ID = 'message'; + + readonly templateId: string = MarkerMessageColumnRenderer.TEMPLATE_ID; + + renderTemplate(container: HTMLElement): IMarkerHighlightedLabelColumnTemplateData { + const columnElement = DOM.append(container, $('.message')); + const highlightedLabel = new HighlightedLabel(columnElement); + + return { columnElement, highlightedLabel }; + } + + renderElement(element: MarkerTableItem, index: number, templateData: IMarkerHighlightedLabelColumnTemplateData, height: number | undefined): void { + templateData.columnElement.title = element.marker.message; + templateData.highlightedLabel.set(element.marker.message, element.messageMatches); + } + + disposeTemplate(templateData: IMarkerHighlightedLabelColumnTemplateData): void { } +} + +class MarkerFileColumnRenderer implements ITableRenderer<MarkerTableItem, IMarkerFileColumnTemplateData>{ + + static readonly TEMPLATE_ID = 'file'; + + readonly templateId: string = MarkerFileColumnRenderer.TEMPLATE_ID; + + constructor( + @ILabelService private readonly labelService: ILabelService + ) { } + + renderTemplate(container: HTMLElement): IMarkerFileColumnTemplateData { + const columnElement = DOM.append(container, $('.file')); + const fileLabel = new HighlightedLabel(columnElement); + fileLabel.element.classList.add('file-label'); + const positionLabel = new HighlightedLabel(columnElement); + positionLabel.element.classList.add('file-position'); + + return { columnElement, fileLabel, positionLabel }; + } + + renderElement(element: MarkerTableItem, index: number, templateData: IMarkerFileColumnTemplateData, height: number | undefined): void { + const positionLabel = Messages.MARKERS_PANEL_AT_LINE_COL_NUMBER(element.marker.startLineNumber, element.marker.startColumn); + + templateData.columnElement.title = `${this.labelService.getUriLabel(element.marker.resource, { relative: false })} ${positionLabel}`; + templateData.fileLabel.set(this.labelService.getUriLabel(element.marker.resource, { relative: true }), element.fileMatches); + templateData.positionLabel.set(positionLabel, undefined); + } + + disposeTemplate(templateData: IMarkerFileColumnTemplateData): void { } +} + +class MarkerOwnerColumnRenderer implements ITableRenderer<MarkerTableItem, IMarkerHighlightedLabelColumnTemplateData>{ + + static readonly TEMPLATE_ID = 'owner'; + + readonly templateId: string = MarkerOwnerColumnRenderer.TEMPLATE_ID; + + renderTemplate(container: HTMLElement): IMarkerHighlightedLabelColumnTemplateData { + const columnElement = DOM.append(container, $('.owner')); + const highlightedLabel = new HighlightedLabel(columnElement); + return { columnElement, highlightedLabel }; + } + + renderElement(element: MarkerTableItem, index: number, templateData: IMarkerHighlightedLabelColumnTemplateData, height: number | undefined): void { + templateData.columnElement.title = element.marker.owner; + templateData.highlightedLabel.set(element.marker.owner, element.ownerMatches); + } + + disposeTemplate(templateData: IMarkerHighlightedLabelColumnTemplateData): void { } +} + +class MarkersTableVirtualDelegate implements ITableVirtualDelegate<any> { + static readonly HEADER_ROW_HEIGHT = 24; + static readonly ROW_HEIGHT = 24; + readonly headerRowHeight = MarkersTableVirtualDelegate.HEADER_ROW_HEIGHT; + + getHeight(item: any) { + return MarkersTableVirtualDelegate.ROW_HEIGHT; + } +} + +export class MarkersTable extends Disposable implements IProblemsWidget { + + private _itemCount: number = 0; + private readonly table: WorkbenchTable<MarkerTableItem>; + + constructor( + private readonly container: HTMLElement, + private readonly markersViewModel: MarkersViewModel, + private resourceMarkers: ResourceMarkers[], + private filterOptions: FilterOptions, + options: IWorkbenchTableOptions<MarkerTableItem>, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILabelService private readonly labelService: ILabelService, + ) { + super(); + + this.table = this.instantiationService.createInstance(WorkbenchTable, + 'Markers', + this.container, + new MarkersTableVirtualDelegate(), + [ + { + label: '', + tooltip: '', + weight: 0, + minimumWidth: 36, + maximumWidth: 36, + templateId: MarkerSeverityColumnRenderer.TEMPLATE_ID, + project(row: Marker): Marker { return row; } + }, + { + label: localize('codeColumnLabel', "Code"), + tooltip: '', + weight: 1, + minimumWidth: 100, + maximumWidth: 300, + templateId: MarkerCodeColumnRenderer.TEMPLATE_ID, + project(row: Marker): Marker { return row; } + }, + { + label: localize('messageColumnLabel', "Message"), + tooltip: '', + weight: 4, + templateId: MarkerMessageColumnRenderer.TEMPLATE_ID, + project(row: Marker): Marker { return row; } + }, + { + label: localize('fileColumnLabel', "File"), + tooltip: '', + weight: 2, + templateId: MarkerFileColumnRenderer.TEMPLATE_ID, + project(row: Marker): Marker { return row; } + }, + { + label: localize('sourceColumnLabel', "Source"), + tooltip: '', + weight: 1, + minimumWidth: 100, + maximumWidth: 300, + templateId: MarkerOwnerColumnRenderer.TEMPLATE_ID, + project(row: Marker): Marker { return row; } + } + ], + [ + this.instantiationService.createInstance(MarkerSeverityColumnRenderer, this.markersViewModel), + this.instantiationService.createInstance(MarkerCodeColumnRenderer), + this.instantiationService.createInstance(MarkerMessageColumnRenderer), + this.instantiationService.createInstance(MarkerFileColumnRenderer), + this.instantiationService.createInstance(MarkerOwnerColumnRenderer), + ], + options + ) as WorkbenchTable<MarkerTableItem>; + + const list = this.table.domNode.querySelector('.monaco-list-rows')! as HTMLElement; + + // mouseover/mouseleave event handlers + const onRowHover = Event.chain(this._register(new DomEmitter(list, 'mouseover')).event) + .map(e => DOM.findParentWithClass(e.target as HTMLElement, 'monaco-list-row', 'monaco-list-rows')) + .filter<HTMLElement>(((e: HTMLElement | null) => !!e) as any) + .map(e => parseInt(e.getAttribute('data-index')!)) + .event; + + const onListLeave = Event.map(this._register(new DomEmitter(list, 'mouseleave')).event, () => -1); + + const onRowHoverOrLeave = Event.latch(Event.any(onRowHover, onListLeave)); + const onRowPermanentHover = Event.debounce(onRowHoverOrLeave, (_, e) => e, 500); + + this._register(onRowPermanentHover(e => { + if (e !== -1 && this.table.row(e)) { + this.markersViewModel.onMarkerMouseHover(this.table.row(e)); + } + })); + } + + get contextKeyService(): IContextKeyService { + return this.table.contextKeyService; + } + + get onContextMenu(): Event<ITableContextMenuEvent<MarkerTableItem>> { + return this.table.onContextMenu; + } + + get onDidOpen(): Event<IOpenEvent<MarkerTableItem | undefined>> { + return this.table.onDidOpen; + } + + get onDidChangeFocus(): Event<ITableEvent<MarkerTableItem>> { + return this.table.onDidChangeFocus; + } + + get onDidChangeSelection(): Event<ITableEvent<MarkerTableItem>> { + return this.table.onDidChangeSelection; + } + + collapseMarkers(): void { } + + domFocus(): void { + this.table.domFocus(); + } + + filterMarkers(resourceMarkers: ResourceMarkers[], filterOptions: FilterOptions): void { + this.filterOptions = filterOptions; + this.reset(resourceMarkers); + } + + getFocus(): (MarkerTableItem | null)[] { + const focus = this.table.getFocus(); + return focus.length > 0 ? [...focus.map(f => this.table.row(f))] : []; + } + + getHTMLElement(): HTMLElement { + return this.table.getHTMLElement(); + } + + getRelativeTop(marker: MarkerTableItem | null): number | null { + return marker ? this.table.getRelativeTop(this.table.indexOf(marker)) : null; + } + + getSelection(): (MarkerTableItem | null)[] { + const selection = this.table.getSelection(); + return selection.length > 0 ? [...selection.map(i => this.table.row(i))] : []; + } + + getVisibleItemCount(): number { + return this._itemCount; + } + + isVisible(): boolean { + return !this.container.classList.contains('hidden'); + } + + layout(height: number, width: number): void { + this.container.style.height = `${height}px`; + this.table.layout(height, width); + } + + reset(resourceMarkers: ResourceMarkers[]): void { + this.resourceMarkers = resourceMarkers; + + const items: MarkerTableItem[] = []; + for (const resourceMarker of this.resourceMarkers) { + for (const marker of resourceMarker.markers) { + if (marker.resource.scheme === network.Schemas.walkThrough || marker.resource.scheme === network.Schemas.walkThroughSnippet) { + continue; + } + + // Exclude pattern + if (this.filterOptions.excludesMatcher.matches(marker.resource)) { + continue; + } + + // Include pattern + if (this.filterOptions.includesMatcher.matches(marker.resource)) { + items.push(new MarkerTableItem(marker)); + continue; + } + + // Severity filter + const matchesSeverity = this.filterOptions.showErrors && MarkerSeverity.Error === marker.marker.severity || + this.filterOptions.showWarnings && MarkerSeverity.Warning === marker.marker.severity || + this.filterOptions.showInfos && MarkerSeverity.Info === marker.marker.severity; + + if (!matchesSeverity) { + continue; + } + + // Text filter + if (this.filterOptions.textFilter.text) { + const sourceMatches = marker.marker.source ? FilterOptions._filter(this.filterOptions.textFilter.text, marker.marker.source) ?? undefined : undefined; + const codeMatches = marker.marker.code ? FilterOptions._filter(this.filterOptions.textFilter.text, typeof marker.marker.code === 'string' ? marker.marker.code : marker.marker.code.value) ?? undefined : undefined; + const messageMatches = FilterOptions._messageFilter(this.filterOptions.textFilter.text, marker.marker.message) ?? undefined; + const fileMatches = FilterOptions._messageFilter(this.filterOptions.textFilter.text, this.labelService.getUriLabel(marker.resource, { relative: true })) ?? undefined; + const ownerMatches = FilterOptions._messageFilter(this.filterOptions.textFilter.text, marker.marker.owner) ?? undefined; + + const matched = sourceMatches || codeMatches || messageMatches || fileMatches || ownerMatches; + if ((matched && !this.filterOptions.textFilter.negate) || (!matched && this.filterOptions.textFilter.negate)) { + items.push(new MarkerTableItem(marker, sourceMatches, codeMatches, messageMatches, fileMatches, ownerMatches)); + } + + continue; + } + + items.push(new MarkerTableItem(marker)); + } + } + this._itemCount = items.length; + this.table.splice(0, Number.POSITIVE_INFINITY, items.sort((a, b) => { + let result = MarkerSeverity.compare(a.marker.severity, b.marker.severity); + + if (result === 0) { + result = compareMarkersByUri(a.marker, b.marker); + } + + if (result === 0) { + result = Range.compareRangesUsingStarts(a.marker, b.marker); + } + + return result; + })); + } + + revealMarkers(activeResource: ResourceMarkers | null, focus: boolean, lastSelectedRelativeTop: number): void { + if (activeResource) { + const activeResourceIndex = this.resourceMarkers.indexOf(activeResource); + + if (activeResourceIndex !== -1) { + if (this.hasSelectedMarkerFor(activeResource)) { + const tableSelection = this.table.getSelection(); + this.table.reveal(tableSelection[0], lastSelectedRelativeTop); + + if (focus) { + this.table.setFocus(tableSelection); + } + } else { + this.table.reveal(activeResourceIndex, 0); + + if (focus) { + this.table.setFocus([activeResourceIndex]); + this.table.setSelection([activeResourceIndex]); + } + } + } + } else if (focus) { + this.table.setSelection([]); + this.table.focusFirst(); + } + } + + setAriaLabel(label: string): void { + this.table.domNode.ariaLabel = label; + } + + setMarkerSelection(selection?: Marker[], focus?: Marker[]): void { + if (this.isVisible()) { + if (selection && selection.length > 0) { + this.table.setSelection(selection.map(m => this.findMarkerIndex(m))); + + if (focus && focus.length > 0) { + this.table.setFocus(focus.map(f => this.findMarkerIndex(f))); + } else { + this.table.setFocus([this.findMarkerIndex(selection[0])]); + } + + this.table.reveal(this.findMarkerIndex(selection[0])); + } else if (this.getSelection().length === 0 && this.getVisibleItemCount() > 0) { + this.table.setSelection([0]); + this.table.setFocus([0]); + this.table.reveal(0); + } + } + } + + toggleVisibility(hide: boolean): void { + this.container.classList.toggle('hidden', hide); + } + + update(resourceMarkers: ResourceMarkers[]): void { + for (const resourceMarker of resourceMarkers) { + const index = this.resourceMarkers.indexOf(resourceMarker); + this.resourceMarkers.splice(index, 1, resourceMarker); + } + this.reset(this.resourceMarkers); + } + + updateMarker(marker: Marker): void { + this.table.rerender(); + } + + private findMarkerIndex(marker: Marker): number { + for (let index = 0; index < this.table.length; index++) { + if (this.table.row(index).marker === marker.marker) { + return index; + } + } + + return -1; + } + + private hasSelectedMarkerFor(resource: ResourceMarkers): boolean { + const selectedElement = this.getSelection(); + if (selectedElement && selectedElement.length > 0) { + if (selectedElement[0] instanceof Marker) { + if (resource.has((<Marker>selectedElement[0]).marker.resource)) { + return true; + } + } + } + + return false; + } +} diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 5b9b0c78abf..1652365c097 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -10,7 +10,7 @@ import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { ResourceMarkers, Marker, RelatedInformation, MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { ResourceMarkers, Marker, RelatedInformation, MarkerElement, MarkerTableItem } from 'vs/workbench/contrib/markers/browser/markersModel'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; @@ -34,10 +34,10 @@ import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/com import { IModelService } from 'vs/editor/common/services/model'; import { Range } from 'vs/editor/common/core/range'; import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types'; +import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/browser/types'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { applyCodeAction } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; +import { applyCodeAction, ApplyCodeActionReason } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { CodeActionTriggerType } from 'vs/editor/common/languages'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -48,6 +48,8 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Link } from 'vs/platform/opener/browser/link'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MarkersContextKeys, MarkersViewMode } from 'vs/workbench/contrib/markers/common/markers'; interface IResourceMarkersTemplateData { resourceLabel: IResourceLabel; @@ -65,7 +67,7 @@ interface IRelatedInformationTemplateData { description: HighlightedLabel; } -export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvider<MarkerElement> { +export class MarkersWidgetAccessibilityProvider implements IListAccessibilityProvider<MarkerElement | MarkerTableItem> { constructor(@ILabelService private readonly labelService: ILabelService) { } @@ -73,12 +75,12 @@ export class MarkersTreeAccessibilityProvider implements IListAccessibilityProvi return localize('problemsView', "Problems View"); } - public getAriaLabel(element: MarkerElement): string | null { + public getAriaLabel(element: MarkerElement | MarkerTableItem): string | null { if (element instanceof ResourceMarkers) { const path = this.labelService.getUriLabel(element.resource, { relative: true }) || element.resource.fsPath; return Messages.MARKERS_TREE_ARIA_LABEL_RESOURCE(element.markers.length, element.name, paths.dirname(path)); } - if (element instanceof Marker) { + if (element instanceof Marker || element instanceof MarkerTableItem) { return Messages.MARKERS_TREE_ARIA_LABEL_MARKER(element); } if (element instanceof RelatedInformation) { @@ -265,9 +267,7 @@ class ToggleMultilineActionViewItem extends ActionViewItem { } private updateExpandedAttribute(): void { - if (this.element) { - this.element.setAttribute('aria-expanded', `${this._action.class === ThemeIcon.asClassName(expandedIcon)}`); - } + this.element?.setAttribute('aria-expanded', `${this._action.class === ThemeIcon.asClassName(expandedIcon)}`); } } @@ -627,7 +627,7 @@ export class MarkerViewModel extends Disposable { if (!this.codeActionsPromise) { this.codeActionsPromise = createCancelablePromise(cancellationToken => { return getCodeActions(this.languageFeaturesService.codeActionProvider, model, new Range(this.marker.range.startLineNumber, this.marker.range.startColumn, this.marker.range.endLineNumber, this.marker.range.endColumn), { - type: CodeActionTriggerType.Invoke, filter: { include: CodeActionKind.QuickFix } + type: CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.ProblemsView, filter: { include: CodeActionKind.QuickFix } }, Progress.None, cancellationToken).then(actions => { return this._register(actions); }); @@ -647,7 +647,7 @@ export class MarkerViewModel extends Disposable { true, () => { return this.openFileAtMarker(this.marker) - .then(() => this.instantiationService.invokeFunction(applyCodeAction, item)); + .then(() => this.instantiationService.invokeFunction(applyCodeAction, item, ApplyCodeActionReason.FromProblemsView)); })); } @@ -693,6 +693,9 @@ export class MarkersViewModel extends Disposable { private readonly _onDidChange: Emitter<Marker | undefined> = this._register(new Emitter<Marker | undefined>()); readonly onDidChange: Event<Marker | undefined> = this._onDidChange.event; + private readonly _onDidChangeViewMode: Emitter<MarkersViewMode> = this._register(new Emitter<MarkersViewMode>()); + readonly onDidChangeViewMode: Event<MarkersViewMode> = this._onDidChangeViewMode.event; + private readonly markersViewStates: Map<string, { viewModel: MarkerViewModel; disposables: IDisposable[] }> = new Map<string, { viewModel: MarkerViewModel; disposables: IDisposable[] }>(); private readonly markersPerResource: Map<string, Marker[]> = new Map<string, Marker[]>(); @@ -700,13 +703,20 @@ export class MarkersViewModel extends Disposable { private hoveredMarker: Marker | null = null; private hoverDelayer: Delayer<void> = new Delayer<void>(300); + private viewModeContextKey: IContextKey<MarkersViewMode>; constructor( multiline: boolean = true, - @IInstantiationService private instantiationService: IInstantiationService + viewMode: MarkersViewMode = MarkersViewMode.Tree, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); this._multiline = multiline; + this._viewMode = viewMode; + + this.viewModeContextKey = MarkersContextKeys.MarkersViewModeContextKey.bindTo(this.contextKeyService); + this.viewModeContextKey.set(viewMode); } add(marker: Marker): void { @@ -789,6 +799,21 @@ export class MarkersViewModel extends Disposable { } } + private _viewMode: MarkersViewMode = MarkersViewMode.Tree; + get viewMode(): MarkersViewMode { + return this._viewMode; + } + + set viewMode(value: MarkersViewMode) { + if (this._viewMode === value) { + return; + } + + this._viewMode = value; + this._onDidChangeViewMode.fire(value); + this.viewModeContextKey.set(value); + } + override dispose(): void { this.markersViewStates.forEach(({ disposables }) => dispose(disposables)); this.markersViewStates.clear(); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 17e438b53ce..bf75c6804ca 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -10,8 +10,7 @@ import * as dom from 'vs/base/browser/dom'; import { IAction, Action, Separator } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import Constants from 'vs/workbench/contrib/markers/browser/constants'; -import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri, MarkerElement } from 'vs/workbench/contrib/markers/browser/markersModel'; +import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent, MarkersModel, compareMarkersByUri, MarkerElement, MarkerTableItem } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -23,14 +22,14 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Iterable } from 'vs/base/common/iterator'; -import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer, ITreeEvent } from 'vs/base/browser/ui/tree/tree'; import { Relay, Event, Emitter } from 'vs/base/common/event'; -import { WorkbenchObjectTree, IListService, IWorkbenchObjectTreeOptions } from 'vs/platform/list/browser/listService'; +import { WorkbenchObjectTree, IListService, IWorkbenchObjectTreeOptions, IOpenEvent } from 'vs/platform/list/browser/listService'; import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilterOptions'; import { IExpression } from 'vs/base/common/glob'; import { deepClone } from 'vs/base/common/objects'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, MarkersTreeAccessibilityProvider, MarkersViewModel } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; +import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, MarkersWidgetAccessibilityProvider, MarkersViewModel } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionBar, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; @@ -40,7 +39,7 @@ import { ResourceLabels } from 'vs/workbench/browser/labels'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { MementoObject, Memento } from 'vs/workbench/common/memento'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { KeyCode } from 'vs/base/common/keyCodes'; import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; @@ -57,6 +56,9 @@ import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/ed import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ResourceListDnDHandler } from 'vs/workbench/browser/dnd'; +import { ITableContextMenuEvent, ITableEvent } from 'vs/base/browser/ui/table/table'; +import { MarkersTable } from 'vs/workbench/contrib/markers/browser/markersTable'; +import { Markers, MarkersContextKeys, MarkersViewMode } from 'vs/workbench/contrib/markers/common/markers'; function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable<ITreeElement<MarkerElement>> { return Iterable.map(resourceMarkers.markers, m => { @@ -67,6 +69,33 @@ function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterab }); } +export interface IProblemsWidget { + get contextKeyService(): IContextKeyService; + + get onContextMenu(): Event<ITreeContextMenuEvent<MarkerElement | null>> | Event<ITableContextMenuEvent<MarkerTableItem>>; + get onDidChangeFocus(): Event<ITreeEvent<MarkerElement | null>> | Event<ITableEvent<MarkerTableItem>>; + get onDidChangeSelection(): Event<ITreeEvent<MarkerElement | null>> | Event<ITableEvent<MarkerTableItem>>; + get onDidOpen(): Event<IOpenEvent<MarkerElement | MarkerTableItem | undefined>>; + + collapseMarkers(): void; + dispose(): void; + domFocus(): void; + filterMarkers(resourceMarkers: ResourceMarkers[], filterOptions: FilterOptions): void; + getFocus(): (MarkerElement | MarkerTableItem | null)[]; + getHTMLElement(): HTMLElement; + getRelativeTop(location: MarkerElement | MarkerTableItem | null): number | null; + getSelection(): (MarkerElement | MarkerTableItem | null)[]; + getVisibleItemCount(): number; + layout(height: number, width: number): void; + reset(resourceMarkers: ResourceMarkers[]): void; + revealMarkers(activeResource: ResourceMarkers | null, focus: boolean, lastSelectedRelativeTop: number): void; + setAriaLabel(label: string): void; + setMarkerSelection(selection?: Marker[], focus?: Marker[]): void; + toggleVisibility(hide: boolean): void; + update(resourceMarkers: ResourceMarkers[]): void; + updateMarker(marker: Marker): void; +} + export class MarkersView extends ViewPane implements IMarkersView { private lastSelectedRelativeTop: number = 0; @@ -77,12 +106,18 @@ export class MarkersView extends ViewPane implements IMarkersView { private readonly filter: Filter; private readonly onVisibleDisposables = this._register(new DisposableStore()); - private tree: MarkersTree | undefined; + private widget!: IProblemsWidget; + private widgetDisposables = this._register(new DisposableStore()); + private widgetContainer!: HTMLElement; + private widgetIdentityProvider: IIdentityProvider<MarkerElement | MarkerTableItem>; + private widgetAccessibilityProvider: MarkersWidgetAccessibilityProvider; private filterActionBar: ActionBar | undefined; private messageBoxContainer: HTMLElement | undefined; private ariaLabelElement: HTMLElement | undefined; readonly filters: MarkersFilters; + private currentHeight = 0; + private currentWidth = 0; private readonly panelState: MementoObject; private _onDidChangeFilterStats = this._register(new Emitter<{ total: number; filtered: number }>()); @@ -122,12 +157,16 @@ export class MarkersView extends ViewPane implements IMarkersView { @IThemeService themeService: IThemeService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.smallLayoutContextKey = Constants.MarkersViewSmallLayoutContextKey.bindTo(this.contextKeyService); - this.panelState = new Memento(Constants.MARKERS_VIEW_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE, StorageTarget.USER); + this.smallLayoutContextKey = MarkersContextKeys.MarkersViewSmallLayoutContextKey.bindTo(this.contextKeyService); + this.panelState = new Memento(Markers.MARKERS_VIEW_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE, StorageTarget.USER); this.markersModel = this._register(instantiationService.createInstance(MarkersModel)); - this.markersViewModel = this._register(instantiationService.createInstance(MarkersViewModel, this.panelState['multiline'])); + this.markersViewModel = this._register(instantiationService.createInstance(MarkersViewModel, this.panelState['multiline'], this.panelState['viewMode'] ?? this.getDefaultViewMode())); this._register(this.onDidChangeVisibility(visible => this.onDidChangeMarkersViewVisibility(visible))); + this._register(this.markersViewModel.onDidChangeViewMode(_ => this.onDidChangeViewMode())); + + this.widgetAccessibilityProvider = instantiationService.createInstance(MarkersWidgetAccessibilityProvider); + this.widgetIdentityProvider = { getId(element: MarkerElement | MarkerTableItem) { return element.id; } }; this.setCurrentActiveEditor(); @@ -144,23 +183,38 @@ export class MarkersView extends ViewPane implements IMarkersView { activeFile: !!this.panelState['activeFile'], layout: new dom.Dimension(0, 0) })); + + // Update filter, whenever the "files.exclude" setting is changed + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (this.filters.excludedFiles && e.affectsConfiguration('files.exclude')) { + this.updateFilter(); + } + })); } public override renderBody(parent: HTMLElement): void { super.renderBody(parent); parent.classList.add('markers-panel'); + this._register(dom.addDisposableListener(parent, 'keydown', e => { + if (this.keybindingService.mightProducePrintableCharacter(new StandardKeyboardEvent(e))) { + this.focusFilter(); + } + })); - const container = dom.append(parent, dom.$('.markers-panel-container')); - - this.createFilterActionBar(container); - this.createArialLabelElement(container); - this.createMessageBox(container); - this.createTree(container); + const panelContainer = dom.append(parent, dom.$('.markers-panel-container')); - this.updateFilter(); + this.createArialLabelElement(panelContainer); + this.createFilterActionBar(panelContainer); this.filterActionBar!.push(new Action(`workbench.actions.treeView.${this.id}.filter`)); + + this.createMessageBox(panelContainer); + + this.widgetContainer = dom.append(panelContainer, dom.$('.widget-container')); + this.createWidget(this.widgetContainer); + + this.updateFilter(); this.renderContent(); } @@ -168,35 +222,34 @@ export class MarkersView extends ViewPane implements IMarkersView { return Messages.MARKERS_PANEL_TITLE_PROBLEMS; } - public override layoutBody(height: number, width: number): void { + public override layoutBody(height: number = this.currentHeight, width: number = this.currentWidth): void { super.layoutBody(height, width); const wasSmallLayout = this.smallLayout; this.smallLayout = width < 600 && height > 100; if (this.smallLayout !== wasSmallLayout) { - if (this.filterActionBar) { - this.filterActionBar.getContainer().classList.toggle('hide', !this.smallLayout); - } + this.filterActionBar?.getContainer().classList.toggle('hide', !this.smallLayout); } const contentHeight = this.smallLayout ? height - 44 : height; - if (this.tree) { - this.tree.layout(contentHeight, width); - } if (this.messageBoxContainer) { this.messageBoxContainer.style.height = `${contentHeight}px`; } + this.widget.layout(contentHeight, width); this.filters.layout = new dom.Dimension(this.smallLayout ? width : width - 200, height); + + this.currentHeight = height; + this.currentWidth = width; } public override focus(): void { - if (this.tree && this.tree.getHTMLElement() === document.activeElement) { + if (this.widget.getHTMLElement() === document.activeElement) { return; } - if (this.hasNoProblems() && this.messageBoxContainer) { - this.messageBoxContainer.focus(); - } else if (this.tree) { - this.tree.domFocus(); - this.setTreeSelection(); + if (this.hasNoProblems()) { + this.messageBoxContainer!.focus(); + } else { + this.widget.domFocus(); + this.widget.setMarkerSelection(); } } @@ -217,7 +270,9 @@ export class MarkersView extends ViewPane implements IMarkersView { public openFileAtElement(element: any, preserveFocus: boolean, sideByside: boolean, pinned: boolean): boolean { const { resource, selection } = element instanceof Marker ? { resource: element.resource, selection: element.range } : - element instanceof RelatedInformation ? { resource: element.raw.resource, selection: element.raw } : { resource: null, selection: null }; + element instanceof RelatedInformation ? { resource: element.raw.resource, selection: element.raw } : + 'marker' in element ? { resource: element.marker.resource, selection: element.marker.range } : + { resource: null, selection: null }; if (resource && selection) { this.editorService.openEditor({ resource, @@ -242,57 +297,36 @@ export class MarkersView extends ViewPane implements IMarkersView { } private refreshPanel(markerOrChange?: Marker | MarkerChangesEvent): void { - if (this.isVisible() && this.tree) { - const hasSelection = this.tree.getSelection().length > 0; - this.cachedFilterStats = undefined; + if (this.isVisible()) { + const hasSelection = this.widget.getSelection().length > 0; if (markerOrChange) { if (markerOrChange instanceof Marker) { - this.tree.rerender(markerOrChange); + this.widget.updateMarker(markerOrChange); } else { if (markerOrChange.added.size || markerOrChange.removed.size) { - // Reset complete tree - this.resetTree(); + // Reset complete widget + this.resetWidget(); } else { // Update resource - for (const updated of markerOrChange.updated) { - this.tree.setChildren(updated, createResourceMarkersIterator(updated), { - /* Pass Identitiy Provider only when updating while the tree is visible */ - diffIdentityProvider: { - getId(element: MarkerElement): string { return element.id; } - } - }); - this.tree.rerender(updated); - } + this.widget.update([...markerOrChange.updated]); } } } else { - // Reset complete tree - this.resetTree(); + // Reset complete widget + this.resetWidget(); } - const { total, filtered } = this.getFilterStats(); - this.tree.toggleVisibility(total === 0 || filtered === 0); - this.renderMessage(); - this._onDidChangeFilterStats.fire(this.getFilterStats()); - if (hasSelection) { - this.setTreeSelection(); + this.widget.setMarkerSelection(); } - } - } - private setTreeSelection(): void { - if (this.tree && this.tree.isVisible() && this.tree.getSelection().length === 0) { - const firstVisibleElement = this.tree.firstVisibleElement; - const marker = firstVisibleElement ? - firstVisibleElement instanceof ResourceMarkers ? firstVisibleElement.markers[0] : - firstVisibleElement instanceof Marker ? firstVisibleElement : undefined - : undefined; - if (marker) { - this.tree.setFocus([marker]); - this.tree.setSelection([marker]); - } + this.cachedFilterStats = undefined; + const { total, filtered } = this.getFilterStats(); + this.toggleVisibility(total === 0 || filtered === 0); + this.renderMessage(); + + this._onDidChangeFilterStats.fire(this.getFilterStats()); } } @@ -300,37 +334,31 @@ export class MarkersView extends ViewPane implements IMarkersView { this.refreshPanel(marker); } - private resetTree(): void { - if (!this.tree) { - return; - } - let resourceMarkers: ResourceMarkers[] = []; - if (this.filters.activeFile) { - if (this.currentActiveResource) { - const activeResourceMarkers = this.markersModel.getResourceMarkers(this.currentActiveResource); - if (activeResourceMarkers) { - resourceMarkers = [activeResourceMarkers]; - } - } - } else { - resourceMarkers = this.markersModel.resourceMarkers; - } - this.tree.setChildren(null, Iterable.map(resourceMarkers, m => ({ element: m, children: createResourceMarkersIterator(m) }))); + private resetWidget(): void { + this.widget.reset(this.getResourceMarkers()); } private updateFilter() { - this.cachedFilterStats = undefined; this.filter.options = new FilterOptions(this.filters.filterText, this.getFilesExcludeExpressions(), this.filters.showWarnings, this.filters.showErrors, this.filters.showInfos, this.uriIdentityService); - if (this.tree) { - this.tree.refilter(); - } - this._onDidChangeFilterStats.fire(this.getFilterStats()); + this.widget.filterMarkers(this.getResourceMarkers(), this.filter.options); + this.cachedFilterStats = undefined; const { total, filtered } = this.getFilterStats(); - if (this.tree) { - this.tree.toggleVisibility(total === 0 || filtered === 0); - } + this.toggleVisibility(total === 0 || filtered === 0); this.renderMessage(); + + this._onDidChangeFilterStats.fire(this.getFilterStats()); + } + + private getDefaultViewMode(): MarkersViewMode { + switch (this.configurationService.getValue<string>('problems.defaultViewMode')) { + case 'table': + return MarkersViewMode.Table; + case 'tree': + return MarkersViewMode.Tree; + default: + return MarkersViewMode.Tree; + } } private getFilesExcludeExpressions(): { root: URI; expression: IExpression }[] | IExpression { @@ -348,6 +376,22 @@ export class MarkersView extends ViewPane implements IMarkersView { return deepClone(this.configurationService.getValue('files.exclude', { resource })) || {}; } + private getResourceMarkers(): ResourceMarkers[] { + if (!this.filters.activeFile) { + return this.markersModel.resourceMarkers; + } + + let resourceMarkers: ResourceMarkers[] = []; + if (this.currentActiveResource) { + const activeResourceMarkers = this.markersModel.getResourceMarkers(this.currentActiveResource); + if (activeResourceMarkers) { + resourceMarkers = [activeResourceMarkers]; + } + } + + return resourceMarkers; + } + private createFilterActionBar(parent: HTMLElement): void { this.filterActionBar = this._register(new ActionBar(parent, { actionViewItemProvider: action => this.getActionViewItem(action) })); this.filterActionBar.getContainer().classList.add('markers-panel-filter-container'); @@ -364,10 +408,65 @@ export class MarkersView extends ViewPane implements IMarkersView { this.ariaLabelElement.setAttribute('id', 'markers-panel-arialabel'); } - private createTree(parent: HTMLElement): void { + private createWidget(parent: HTMLElement): void { + this.widget = this.markersViewModel.viewMode === MarkersViewMode.Table ? this.createTable(parent) : this.createTree(parent); + this.widgetDisposables.add(this.widget); + + const markerFocusContextKey = MarkersContextKeys.MarkerFocusContextKey.bindTo(this.widget.contextKeyService); + const relatedInformationFocusContextKey = MarkersContextKeys.RelatedInformationFocusContextKey.bindTo(this.widget.contextKeyService); + this.widgetDisposables.add(this.widget.onDidChangeFocus(focus => { + markerFocusContextKey.set(focus.elements.some(e => e instanceof Marker)); + relatedInformationFocusContextKey.set(focus.elements.some(e => e instanceof RelatedInformation)); + })); + + this.widgetDisposables.add(Event.debounce(this.widget.onDidOpen, (last, event) => event, 75, true)(options => { + this.openFileAtElement(options.element, !!options.editorOptions.preserveFocus, options.sideBySide, !!options.editorOptions.pinned); + })); + + this.widgetDisposables.add(Event.any<any>(this.widget.onDidChangeSelection, this.widget.onDidChangeFocus)(() => { + const elements = [...this.widget.getSelection(), ...this.widget.getFocus()]; + for (const element of elements) { + if (element instanceof Marker) { + const viewModel = this.markersViewModel.getViewModel(element); + if (viewModel) { + viewModel.showLightBulb(); + } + } + } + })); + + this.widgetDisposables.add(this.widget.onContextMenu(this.onContextMenu, this)); + this.widgetDisposables.add(this.widget.onDidChangeSelection(this.onSelected, this)); + } + + private createTable(parent: HTMLElement): IProblemsWidget { + const table = this.instantiationService.createInstance(MarkersTable, + dom.append(parent, dom.$('.markers-table-container')), + this.markersViewModel, + this.getResourceMarkers(), + this.filter.options, + { + accessibilityProvider: this.widgetAccessibilityProvider, + dnd: this.instantiationService.createInstance(ResourceListDnDHandler, (element) => { + if (element instanceof MarkerTableItem) { + return withSelection(element.resource, element.range); + } + return null; + }), + horizontalScrolling: false, + identityProvider: this.widgetIdentityProvider, + multipleSelectionSupport: true, + selectionNavigation: true + }, + ); + + return table; + } + + private createTree(parent: HTMLElement): IProblemsWidget { const onDidChangeRenderNodeCount = new Relay<ITreeNode<any, any>>(); - const treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); + const treeLabels = this.instantiationService.createInstance(ResourceLabels, this); const virtualDelegate = new VirtualDelegate(this.markersViewModel); const renderers = [ @@ -375,23 +474,16 @@ export class MarkersView extends ViewPane implements IMarkersView { this.instantiationService.createInstance(MarkerRenderer, this.markersViewModel), this.instantiationService.createInstance(RelatedInformationRenderer) ]; - const accessibilityProvider = this.instantiationService.createInstance(MarkersTreeAccessibilityProvider); - - const identityProvider = { - getId(element: MarkerElement) { - return element.id; - } - }; - this.tree = this._register(this.instantiationService.createInstance(MarkersTree, + const tree = this.instantiationService.createInstance(MarkersTree, 'MarkersView', dom.append(parent, dom.$('.tree-container.show-file-icons')), virtualDelegate, renderers, { filter: this.filter, - accessibilityProvider, - identityProvider, + accessibilityProvider: this.widgetAccessibilityProvider, + identityProvider: this.widgetIdentityProvider, dnd: this.instantiationService.createInstance(ResourceListDnDHandler, (element) => { if (element instanceof ResourceMarkers) { return element.resource; @@ -411,65 +503,25 @@ export class MarkersView extends ViewPane implements IMarkersView { selectionNavigation: true, multipleSelectionSupport: true, }, - )); - - onDidChangeRenderNodeCount.input = this.tree.onDidChangeRenderNodeCount; - - const markerFocusContextKey = Constants.MarkerFocusContextKey.bindTo(this.tree.contextKeyService); - const relatedInformationFocusContextKey = Constants.RelatedInformationFocusContextKey.bindTo(this.tree.contextKeyService); - this._register(this.tree.onDidChangeFocus(focus => { - markerFocusContextKey.set(focus.elements.some(e => e instanceof Marker)); - relatedInformationFocusContextKey.set(focus.elements.some(e => e instanceof RelatedInformation)); - })); - - this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, 75, true)(options => { - this.openFileAtElement(options.element, !!options.editorOptions.preserveFocus, options.sideBySide, !!options.editorOptions.pinned); - })); + ); - this._register(this.tree.onContextMenu(this.onContextMenu, this)); + onDidChangeRenderNodeCount.input = tree.onDidChangeRenderNodeCount; - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (this.filters.excludedFiles && e.affectsConfiguration('files.exclude')) { - this.updateFilter(); - } - })); - - // move focus to input, whenever a key is pressed in the panel container - this._register(dom.addDisposableListener(parent, 'keydown', e => { - if (this.keybindingService.mightProducePrintableCharacter(new StandardKeyboardEvent(e))) { - this.focusFilter(); - } - })); - - this._register(Event.any<any>(this.tree.onDidChangeSelection, this.tree.onDidChangeFocus)(() => { - const elements = [...this.tree!.getSelection(), ...this.tree!.getFocus()]; - for (const element of elements) { - if (element instanceof Marker) { - const viewModel = this.markersViewModel.getViewModel(element); - if (viewModel) { - viewModel.showLightBulb(); - } - } - } - })); - - this._register(this.tree.onDidChangeSelection(() => this.onSelected())); + return tree; } collapseAll(): void { - if (this.tree) { - this.tree.collapseAll(); - this.tree.setSelection([]); - this.tree.setFocus([]); - this.tree.getHTMLElement().focus(); - this.tree.focusFirst(); - } + this.widget.collapseMarkers(); } setMultiline(multiline: boolean): void { this.markersViewModel.multiline = multiline; } + setViewMode(viewMode: MarkersViewMode): void { + this.markersViewModel.viewMode = viewMode; + } + private onDidChangeMarkersViewVisibility(visible: boolean): void { this.onVisibleDisposables.clear(); if (visible) { @@ -477,8 +529,6 @@ export class MarkersView extends ViewPane implements IMarkersView { this.onVisibleDisposables.add(disposable); } this.refreshPanel(); - } else if (this.tree) { - this.tree.toggleVisibility(true); } } @@ -546,6 +596,41 @@ export class MarkersView extends ViewPane implements IMarkersView { } } + private onDidChangeViewMode(): void { + if (this.widgetContainer && this.widget) { + this.widgetContainer.textContent = ''; + this.widgetDisposables.clear(); + } + + // Save selection + const selection = new Set<Marker>(); + for (const marker of this.widget.getSelection()) { + if (marker instanceof ResourceMarkers) { + marker.markers.forEach(m => selection.add(m)); + } else if (marker instanceof Marker || marker instanceof MarkerTableItem) { + selection.add(marker); + } + } + + // Save focus + const focus = new Set<Marker>(); + for (const marker of this.widget.getFocus()) { + if (marker instanceof Marker || marker instanceof MarkerTableItem) { + focus.add(marker); + } + } + + // Create new widget + this.createWidget(this.widgetContainer); + this.refreshPanel(); + + // Restore selection + if (selection.size > 0) { + this.widget.setMarkerSelection(Array.from(selection), Array.from(focus)); + this.widget.domFocus(); + } + } + private isCurrentResourceGotAddedToMarkersData(changedResources: URI[]) { const currentlyActiveResource = this.currentActiveResource; if (!currentlyActiveResource) { @@ -572,11 +657,9 @@ export class MarkersView extends ViewPane implements IMarkersView { } private onSelected(): void { - if (this.tree) { - let selection = this.tree.getSelection(); - if (selection && selection.length > 0) { - this.lastSelectedRelativeTop = this.tree!.getRelativeTop(selection[0]) || 0; - } + const selection = this.widget.getSelection(); + if (selection && selection.length > 0) { + this.lastSelectedRelativeTop = this.widget.getRelativeTop(selection[0]) || 0; } } @@ -587,10 +670,8 @@ export class MarkersView extends ViewPane implements IMarkersView { private renderContent(): void { this.cachedFilterStats = undefined; - this.resetTree(); - if (this.tree) { - this.tree.toggleVisibility(this.hasNoProblems()); - } + this.resetWidget(); + this.toggleVisibility(this.hasNoProblems()); this.renderMessage(); } @@ -663,9 +744,7 @@ export class MarkersView extends ViewPane implements IMarkersView { } private setAriaLabel(label: string): void { - if (this.tree) { - this.tree.ariaLabel = label; - } + this.widget.setAriaLabel(label); this.ariaLabelElement!.setAttribute('aria-label', label); } @@ -679,33 +758,13 @@ export class MarkersView extends ViewPane implements IMarkersView { private autoReveal(focus: boolean = false): void { // No need to auto reveal if active file filter is on - if (this.filters.activeFile || !this.tree) { + if (this.filters.activeFile) { return; } - let autoReveal = this.configurationService.getValue<boolean>('problems.autoReveal'); + const autoReveal = this.configurationService.getValue<boolean>('problems.autoReveal'); if (typeof autoReveal === 'boolean' && autoReveal) { - let currentActiveResource = this.getResourceForCurrentActiveResource(); - if (currentActiveResource) { - if (this.tree.hasElement(currentActiveResource)) { - if (!this.tree.isCollapsed(currentActiveResource) && this.hasSelectedMarkerFor(currentActiveResource)) { - this.tree.reveal(this.tree.getSelection()[0], this.lastSelectedRelativeTop); - if (focus) { - this.tree.setFocus(this.tree.getSelection()); - } - } else { - this.tree.expand(currentActiveResource); - this.tree.reveal(currentActiveResource, 0); - - if (focus) { - this.tree.setFocus([currentActiveResource]); - this.tree.setSelection([currentActiveResource]); - } - } - } - } else if (focus) { - this.tree.setSelection([]); - this.tree.focusFirst(); - } + const currentActiveResource = this.getResourceForCurrentActiveResource(); + this.widget.revealMarkers(currentActiveResource, focus, this.lastSelectedRelativeTop); } } @@ -713,29 +772,15 @@ export class MarkersView extends ViewPane implements IMarkersView { return this.currentActiveResource ? this.markersModel.getResourceMarkers(this.currentActiveResource) : null; } - private hasSelectedMarkerFor(resource: ResourceMarkers): boolean { - if (this.tree) { - let selectedElement = this.tree.getSelection(); - if (selectedElement && selectedElement.length > 0) { - if (selectedElement[0] instanceof Marker) { - if (resource.has((<Marker>selectedElement[0]).marker.resource)) { - return true; - } - } - } - } - return false; - } - private updateRangeHighlights() { this.rangeHighlightDecorations.removeHighlightRange(); - if (this.tree && this.tree.getHTMLElement() === document.activeElement) { + if (this.widget.getHTMLElement() === document.activeElement) { this.highlightCurrentSelectedMarkerRange(); } } private highlightCurrentSelectedMarkerRange() { - const selections = this.tree ? this.tree.getSelection() : []; + const selections = this.widget.getSelection() ?? []; if (selections.length !== 1) { return; @@ -750,13 +795,18 @@ export class MarkersView extends ViewPane implements IMarkersView { this.rangeHighlightDecorations.highlightRange(selection); } - private onContextMenu(e: ITreeContextMenuEvent<MarkerElement | null>): void { + private onContextMenu(e: ITreeContextMenuEvent<MarkerElement | null> | ITableContextMenuEvent<MarkerTableItem>): void { + const element = e.element; + if (!element) { + return; + } + e.browserEvent.preventDefault(); e.browserEvent.stopPropagation(); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor!, - getActions: () => this.getMenuActions(e.element), + getActions: () => this.getMenuActions(element), getActionViewItem: (action) => { const keybinding = this.keybindingService.lookupKeybinding(action.id); if (keybinding) { @@ -766,7 +816,7 @@ export class MarkersView extends ViewPane implements IMarkersView { }, onHide: (wasCancelled?: boolean) => { if (wasCancelled) { - this.tree!.domFocus(); + this.widget.domFocus(); } } }); @@ -786,14 +836,14 @@ export class MarkersView extends ViewPane implements IMarkersView { } } - const menu = this.menuService.createMenu(MenuId.ProblemsPanelContext, this.tree!.contextKeyService); + const menu = this.menuService.createMenu(MenuId.ProblemsPanelContext, this.widget.contextKeyService); createAndFillInContextMenuActions(menu, undefined, result); menu.dispose(); return result; } public getFocusElement(): MarkerElement | undefined { - return this.tree?.getFocus()[0] || undefined; + return this.widget.getFocus()[0] ?? undefined; } public getFocusedSelectedElements(): MarkerElement[] | null { @@ -801,7 +851,7 @@ export class MarkersView extends ViewPane implements IMarkersView { if (!focus) { return null; } - const selection = this.tree!.getSelection(); + const selection = this.widget.getSelection(); if (selection.includes(focus)) { const result: MarkerElement[] = []; for (const selected of selection) { @@ -828,27 +878,18 @@ export class MarkersView extends ViewPane implements IMarkersView { getFilterStats(): { total: number; filtered: number } { if (!this.cachedFilterStats) { - this.cachedFilterStats = this.computeFilterStats(); + this.cachedFilterStats = { + total: this.markersModel.total, + filtered: this.widget?.getVisibleItemCount() ?? 0 + }; } return this.cachedFilterStats; } - private computeFilterStats(): { total: number; filtered: number } { - let filtered = 0; - if (this.tree) { - const root = this.tree.getNode(); - - for (const resourceMarkerNode of root.children) { - for (const markerNode of resourceMarkerNode.children) { - if (resourceMarkerNode.visible && markerNode.visible) { - filtered++; - } - } - } - } - - return { total: this.markersModel.total, filtered }; + private toggleVisibility(hide: boolean): void { + this.widget.toggleVisibility(hide); + this.layoutBody(); } override saveState(): void { @@ -860,6 +901,7 @@ export class MarkersView extends ViewPane implements IMarkersView { this.panelState['useFilesExclude'] = this.filters.excludedFiles; this.panelState['activeFile'] = this.filters.activeFile; this.panelState['multiline'] = this.markersViewModel.multiline; + this.panelState['viewMode'] = this.markersViewModel.viewMode; super.saveState(); } @@ -870,13 +912,13 @@ export class MarkersView extends ViewPane implements IMarkersView { } -class MarkersTree extends WorkbenchObjectTree<MarkerElement, FilterData> { +class MarkersTree extends WorkbenchObjectTree<MarkerElement, FilterData> implements IProblemsWidget { private readonly visibilityContextKey: IContextKey<boolean>; constructor( user: string, - readonly container: HTMLElement, + private readonly container: HTMLElement, delegate: IListVirtualDelegate<MarkerElement>, renderers: ITreeRenderer<MarkerElement, FilterData, any>[], options: IWorkbenchObjectTreeOptions<MarkerElement, FilterData>, @@ -888,12 +930,38 @@ class MarkersTree extends WorkbenchObjectTree<MarkerElement, FilterData> { @IAccessibilityService accessibilityService: IAccessibilityService ) { super(user, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService, accessibilityService); - this.visibilityContextKey = Constants.MarkersTreeVisibilityContextKey.bindTo(contextKeyService); + this.visibilityContextKey = MarkersContextKeys.MarkersTreeVisibilityContextKey.bindTo(contextKeyService); } - override layout(height: number, width: number): void { - this.container.style.height = `${height}px`; - super.layout(height, width); + collapseMarkers(): void { + this.collapseAll(); + this.setSelection([]); + this.setFocus([]); + this.getHTMLElement().focus(); + this.focusFirst(); + } + + filterMarkers(): void { + this.refilter(); + } + + getVisibleItemCount(): number { + let filtered = 0; + const root = this.getNode(); + + for (const resourceMarkerNode of root.children) { + for (const markerNode of resourceMarkerNode.children) { + if (resourceMarkerNode.visible && markerNode.visible) { + filtered++; + } + } + } + + return filtered; + } + + isVisible(): boolean { + return !this.container.classList.contains('hidden'); } toggleVisibility(hide: boolean): void { @@ -901,10 +969,110 @@ class MarkersTree extends WorkbenchObjectTree<MarkerElement, FilterData> { this.container.classList.toggle('hidden', hide); } - isVisible(): boolean { - return !this.container.classList.contains('hidden'); + reset(resourceMarkers: ResourceMarkers[]): void { + this.setChildren(null, Iterable.map(resourceMarkers, m => ({ element: m, children: createResourceMarkersIterator(m) }))); + } + + revealMarkers(activeResource: ResourceMarkers | null, focus: boolean, lastSelectedRelativeTop: number): void { + if (activeResource) { + if (this.hasElement(activeResource)) { + if (!this.isCollapsed(activeResource) && this.hasSelectedMarkerFor(activeResource)) { + this.reveal(this.getSelection()[0], lastSelectedRelativeTop); + if (focus) { + this.setFocus(this.getSelection()); + } + } else { + this.expand(activeResource); + this.reveal(activeResource, 0); + + if (focus) { + this.setFocus([activeResource]); + this.setSelection([activeResource]); + } + } + } + } else if (focus) { + this.setSelection([]); + this.focusFirst(); + } + } + + setAriaLabel(label: string): void { + this.ariaLabel = label; + } + + setMarkerSelection(selection?: Marker[], focus?: Marker[]): void { + if (this.isVisible()) { + if (selection && selection.length > 0) { + this.setSelection(selection.map(m => this.findMarkerNode(m))); + + if (focus && focus.length > 0) { + this.setFocus(focus.map(f => this.findMarkerNode(f))); + } else { + this.setFocus([this.findMarkerNode(selection[0])]); + } + + this.reveal(this.findMarkerNode(selection[0])); + } else if (this.getSelection().length === 0) { + const firstVisibleElement = this.firstVisibleElement; + const marker = firstVisibleElement ? + firstVisibleElement instanceof ResourceMarkers ? firstVisibleElement.markers[0] : + firstVisibleElement instanceof Marker ? firstVisibleElement : undefined + : undefined; + + if (marker) { + this.setSelection([marker]); + this.setFocus([marker]); + this.reveal(marker); + } + } + } + } + + update(resourceMarkers: ResourceMarkers[]): void { + for (const resourceMarker of resourceMarkers) { + this.setChildren(resourceMarker, createResourceMarkersIterator(resourceMarker)); + this.rerender(resourceMarker); + } + } + + updateMarker(marker: Marker): void { + this.rerender(marker); } + private findMarkerNode(marker: Marker) { + for (const resourceNode of this.getNode().children) { + for (const markerNode of resourceNode.children) { + if (markerNode.element instanceof Marker && markerNode.element.marker === marker.marker) { + return markerNode.element; + } + } + } + + return null; + } + + private hasSelectedMarkerFor(resource: ResourceMarkers): boolean { + const selectedElement = this.getSelection(); + if (selectedElement && selectedElement.length > 0) { + if (selectedElement[0] instanceof Marker) { + if (resource.has((<Marker>selectedElement[0]).marker.resource)) { + return true; + } + } + } + + return false; + } + + override dispose(): void { + super.dispose(); + } + + override layout(height: number, width: number): void { + this.container.style.height = `${height}px`; + super.layout(height, width); + } } registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 63dc3458879..a8857968647 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -11,7 +11,6 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; -import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { IThemeService, registerThemingParticipant, ICssStyleCollector, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; @@ -31,6 +30,7 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IMarkersView } from 'vs/workbench/contrib/markers/browser/markers'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint'; +import { MarkersContextKeys } from 'vs/workbench/contrib/markers/common/markers'; export interface IMarkersFiltersChangeEvent { filterText?: boolean; @@ -260,7 +260,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { ) { super(null, action); this.keybindingService = keybindingService; - this.focusContextKey = Constants.MarkerViewFilterFocusContextKey.bindTo(contextKeyService); + this.focusContextKey = MarkersContextKeys.MarkerViewFilterFocusContextKey.bindTo(contextKeyService); this.delayedFilterUpdate = new Delayer<void>(400); this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); this._register(markersView.onDidFocusFilter(() => this.focus())); diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index 07bf1366e81..4ce704bb880 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -217,3 +217,82 @@ .markers-panel .monaco-tl-contents .actions .action-item.disabled { display: none; } + +/* Table */ + +.markers-panel .markers-table-container .monaco-table .monaco-table-th { + display: flex; + font-weight: 600; + align-items: center; + padding-left: 10px; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td { + display: flex; + align-items: center; + padding-left: 10px; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td .highlight { + font-weight: bold; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .code, +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .message, +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .file, +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .owner { + overflow: hidden; + text-overflow: ellipsis; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .severity { + display: flex; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row.selected .monaco-table-tr > .monaco-table-td.quickFix > .severity, +.markers-panel .markers-table-container .monaco-table .monaco-list-row.focused .monaco-table-tr > .monaco-table-td.quickFix > .severity, +.markers-panel .markers-table-container .monaco-table .monaco-list-row:hover .monaco-table-tr > .monaco-table-td.quickFix > .severity { + display: none; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .actions { + margin-left: -3px; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .actions > .monaco-action-bar .action-item { + display: none; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row.selected .monaco-table-tr > .monaco-table-td.quickFix > .actions > .monaco-action-bar .action-item, +.markers-panel .markers-table-container .monaco-table .monaco-list-row.focused .monaco-table-tr > .monaco-table-td.quickFix > .actions > .monaco-action-bar .action-item, +.markers-panel .markers-table-container .monaco-table .monaco-list-row:hover .monaco-table-tr > .monaco-table-td.quickFix > .actions > .monaco-action-bar .action-item { + display: flex; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .code > .monaco-link::before, +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .code > .code-label::before { + content: '('; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .code > .monaco-link::after, +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .code > .code-label::after { + content: ')'; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .code.code-link > .code-label { + display: none; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .code.code-link > .monaco-link { + display: inline; + text-decoration: underline; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .code > .monaco-link { + display: none; +} + +.markers-panel .markers-table-container .monaco-table .monaco-list-row .monaco-table-tr > .monaco-table-td > .file > .file-position { + margin-left: 6px; + opacity: 0.7; +} diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index e5d0ab10ba4..ed66fa20c78 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -15,6 +15,7 @@ export default class Messages { public static PROBLEMS_PANEL_CONFIGURATION_TITLE: string = nls.localize('problems.panel.configuration.title', "Problems View"); public static PROBLEMS_PANEL_CONFIGURATION_AUTO_REVEAL: string = nls.localize('problems.panel.configuration.autoreveal', "Controls whether Problems view should automatically reveal files when opening them."); + public static PROBLEMS_PANEL_CONFIGURATION_VIEW_MODE: string = nls.localize('problems.panel.configuration.viewMode', "Controls the default view mode of the Problems view."); public static PROBLEMS_PANEL_CONFIGURATION_SHOW_CURRENT_STATUS: string = nls.localize('problems.panel.configuration.showCurrentInStatus', "When enabled shows the current problem in the status bar."); public static PROBLEMS_PANEL_CONFIGURATION_COMPARE_ORDER: string = nls.localize('problems.panel.configuration.compareOrder', "Controls the order in which problems are navigated."); public static PROBLEMS_PANEL_CONFIGURATION_COMPARE_ORDER_SEVERITY: string = nls.localize('problems.panel.configuration.compareOrder.severity', "Navigate problems ordered by severity"); diff --git a/src/vs/workbench/contrib/markers/common/markers.ts b/src/vs/workbench/contrib/markers/common/markers.ts new file mode 100644 index 00000000000..6f3efbbeec6 --- /dev/null +++ b/src/vs/workbench/contrib/markers/common/markers.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const enum MarkersViewMode { + Table = 'table', + Tree = 'tree' +} + +export namespace Markers { + export const MARKERS_CONTAINER_ID = 'workbench.panel.markers'; + export const MARKERS_VIEW_ID = 'workbench.panel.markers.view'; + export const MARKERS_VIEW_STORAGE_ID = 'workbench.panel.markers'; + export const MARKER_COPY_ACTION_ID = 'problems.action.copy'; + export const MARKER_COPY_MESSAGE_ACTION_ID = 'problems.action.copyMessage'; + export const RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID = 'problems.action.copyRelatedInformationMessage'; + export const FOCUS_PROBLEMS_FROM_FILTER = 'problems.action.focusProblemsFromFilter'; + export const MARKERS_VIEW_FOCUS_FILTER = 'problems.action.focusFilter'; + export const MARKERS_VIEW_CLEAR_FILTER_TEXT = 'problems.action.clearFilterText'; + export const MARKERS_VIEW_SHOW_MULTILINE_MESSAGE = 'problems.action.showMultilineMessage'; + export const MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE = 'problems.action.showSinglelineMessage'; + export const MARKER_OPEN_ACTION_ID = 'problems.action.open'; + export const MARKER_OPEN_SIDE_ACTION_ID = 'problems.action.openToSide'; + export const MARKER_SHOW_PANEL_ID = 'workbench.action.showErrorsWarnings'; + export const MARKER_SHOW_QUICK_FIX = 'problems.action.showQuickFixes'; + export const TOGGLE_MARKERS_VIEW_ACTION_ID = 'workbench.actions.view.toggleProblems'; +} + +export namespace MarkersContextKeys { + export const MarkersViewModeContextKey = new RawContextKey<MarkersViewMode>('problemsViewMode', MarkersViewMode.Tree); + export const MarkersViewSmallLayoutContextKey = new RawContextKey<boolean>(`problemsView.smallLayout`, false); + export const MarkersTreeVisibilityContextKey = new RawContextKey<boolean>('problemsVisibility', false); + export const MarkerFocusContextKey = new RawContextKey<boolean>('problemFocus', false); + export const MarkerViewFilterFocusContextKey = new RawContextKey<boolean>('problemsFilterFocus', false); + export const RelatedInformationFocusContextKey = new RawContextKey<boolean>('relatedInformationFocus', false); +} diff --git a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts index 3d018851636..9cb2c9dc650 100644 --- a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts +++ b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts @@ -153,9 +153,9 @@ suite('MarkersModel Test', () => { model.setResourceMarkers([[document, [{ ...aMarker(), resource: frag1 }, { ...aMarker(), resource: frag2 }]]]); assert.strictEqual(model.total, 3); - let a = model.getResourceMarkers(document); - let b = model.getResourceMarkers(frag1); - let c = model.getResourceMarkers(frag2); + const a = model.getResourceMarkers(document); + const b = model.getResourceMarkers(frag1); + const c = model.getResourceMarkers(frag2); assert.ok(a === b); assert.ok(a === c); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts new file mode 100644 index 00000000000..4fa542c981d --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -0,0 +1,304 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { MergeEditorInput, MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; +import { ctxIsMergeEditor, ctxMergeEditorLayout } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class OpenMergeEditor extends Action2 { + constructor() { + super({ + id: '_open.mergeEditor', + title: localize('title', "Open Merge Editor"), + }); + } + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const validatedArgs = IRelaxedOpenArgs.validate(args[0]); + + const instaService = accessor.get(IInstantiationService); + const input = instaService.createInstance( + MergeEditorInput, + validatedArgs.base, + validatedArgs.input1, + validatedArgs.input2, + validatedArgs.output, + ); + accessor.get(IEditorService).openEditor(input, { preserveFocus: true }); + } +} + +namespace IRelaxedOpenArgs { + export function validate(obj: unknown): { + base: URI; + input1: MergeEditorInputData; + input2: MergeEditorInputData; + output: URI; + } { + if (!obj || typeof obj !== 'object') { + throw new TypeError('invalid argument'); + } + + const o = obj as IRelaxedOpenArgs; + const base = toUri(o.base); + const output = toUri(o.output); + const input1 = toInputData(o.input1); + const input2 = toInputData(o.input2); + return { base, input1, input2, output }; + } + + function toInputData(obj: unknown): MergeEditorInputData { + if (typeof obj === 'string') { + return new MergeEditorInputData(URI.parse(obj, true), undefined, undefined, undefined); + } + if (!obj || typeof obj !== 'object') { + throw new TypeError('invalid argument'); + } + + if (isUriComponents(obj)) { + return new MergeEditorInputData(URI.revive(obj), undefined, undefined, undefined); + } + + const o = obj as IRelaxedInputData; + const title = o.title; + const uri = toUri(o.uri); + const detail = o.detail; + const description = o.description; + return new MergeEditorInputData(uri, title, detail, description); + } + + function toUri(obj: unknown): URI { + if (typeof obj === 'string') { + return URI.parse(obj, true); + } else if (obj && typeof obj === 'object') { + return URI.revive(<UriComponents>obj); + } + throw new TypeError('invalid argument'); + } + + function isUriComponents(obj: unknown): obj is UriComponents { + if (!obj || typeof obj !== 'object') { + return false; + } + const o = obj as UriComponents; + return typeof o.scheme === 'string' + && typeof o.authority === 'string' + && typeof o.path === 'string' + && typeof o.query === 'string' + && typeof o.fragment === 'string'; + } +} + +type IRelaxedInputData = { uri: UriComponents; title?: string; detail?: string; description?: string }; + +type IRelaxedOpenArgs = { + base: UriComponents | string; + input1: IRelaxedInputData | string; + input2: IRelaxedInputData | string; + output: UriComponents | string; +}; + +export class SetMixedLayout extends Action2 { + constructor() { + super({ + id: 'merge.mixedLayout', + title: localize('layout.mixed', "Mixed Layout"), + toggled: ctxMergeEditorLayout.isEqualTo('mixed'), + menu: [{ + id: MenuId.EditorTitle, + when: ctxIsMergeEditor, + group: '1_merge', + order: 9, + }], + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + activeEditorPane.setLayout('mixed'); + } + } +} + +export class SetColumnLayout extends Action2 { + constructor() { + super({ + id: 'merge.columnLayout', + title: localize('layout.column', "Column Layout"), + toggled: ctxMergeEditorLayout.isEqualTo('columns'), + menu: [{ + id: MenuId.EditorTitle, + when: ctxIsMergeEditor, + group: '1_merge', + order: 10, + }], + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + activeEditorPane.setLayout('columns'); + } + } +} + +export class GoToNextConflict extends Action2 { + constructor() { + super({ + id: 'merge.goToNextConflict', + category: localize('mergeEditor', "Merge Editor"), + title: localize('merge.goToNextConflict', "Go to Next Conflict"), + icon: Codicon.arrowDown, + menu: [{ + id: MenuId.EditorTitle, + when: ctxIsMergeEditor, + group: 'navigation', + }], + f1: true, + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + activeEditorPane.viewModel.get()?.goToNextConflict(); + } + } +} + +export class GoToPreviousConflict extends Action2 { + constructor() { + super({ + id: 'merge.goToPreviousConflict', + category: localize('mergeEditor', "Merge Editor"), + title: localize('merge.goToPreviousConflict', "Go to Previous Conflict"), + icon: Codicon.arrowUp, + menu: [{ + id: MenuId.EditorTitle, + when: ctxIsMergeEditor, + group: 'navigation', + }], + f1: true, + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + activeEditorPane.viewModel.get()?.goToPreviousConflict(); + } + } +} + +export class ToggleActiveConflictInput1 extends Action2 { + constructor() { + super({ + id: 'merge.toggleActiveConflictInput1', + category: localize('mergeEditor', "Merge Editor"), + title: localize('merge.toggleCurrentConflictFromLeft', "Toggle Current Conflict from Left"), + f1: true, + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + const vm = activeEditorPane.viewModel.get(); + if (!vm) { + return; + } + vm.toggleActiveConflict(1); + } + } +} + +export class ToggleActiveConflictInput2 extends Action2 { + constructor() { + super({ + id: 'merge.toggleActiveConflictInput2', + category: localize('mergeEditor', "Merge Editor"), + title: localize('merge.toggleCurrentConflictFromRight', "Toggle Current Conflict from Right"), + f1: true, + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + const vm = activeEditorPane.viewModel.get(); + if (!vm) { + return; + } + vm.toggleActiveConflict(2); + } + } +} + +export class CompareInput1WithBaseCommand extends Action2 { + constructor() { + super({ + id: 'mergeEditor.compareInput1WithBase', + category: localize('mergeEditor', "Merge Editor"), + title: localize('mergeEditor.compareInput1WithBase', "Compare Input 1 With Base"), + f1: true, + precondition: ctxIsMergeEditor, + }); + } + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const editorService = accessor.get(IEditorService); + const commandService = accessor.get(ICommandService); + mergeEditorCompare(editorService, commandService, 1); + } +} + +export class CompareInput2WithBaseCommand extends Action2 { + constructor() { + super({ + id: 'mergeEditor.compareInput2WithBase', + category: localize('mergeEditor', "Merge Editor"), + title: localize('mergeEditor.compareInput2WithBase', "Compare Input 2 With Base"), + f1: true, + precondition: ctxIsMergeEditor, + }); + } + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const editorService = accessor.get(IEditorService); + const commandService = accessor.get(ICommandService); + mergeEditorCompare(editorService, commandService, 2); + } +} + +function mergeEditorCompare(editorService: IEditorService, commandService: ICommandService, inputNumber: 1 | 2) { + const { activeEditorPane } = editorService; + if (activeEditorPane instanceof MergeEditor) { + if (!activeEditorPane.model) { + return; + } + + const base = activeEditorPane.model.base.uri; + const input = inputNumber === 1 ? activeEditorPane.model.input1.uri : activeEditorPane.model.input2.uri; + + openDiffEditor(commandService, base, input); + } +} + +function openDiffEditor(commandService: ICommandService, left: URI, right: URI, label?: string) { + commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, left, right, label); +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts new file mode 100644 index 00000000000..83a458d15f5 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { Codicon } from 'vs/base/common/codicons'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { ctxIsMergeEditor } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; + +interface MergeEditorContents { + languageId: string; + base: string; + input1: string; + input2: string; + result: string; +} + +export class MergeEditorCopyContentsToJSON extends Action2 { + + constructor() { + super({ + id: 'merge.dev.copyContents', + category: 'Merge Editor (Dev)', + title: localize('merge.dev.copyContents', "Copy Contents of Inputs, Base and Result as JSON"), + icon: Codicon.layoutCentered, + f1: true, + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + const clipboardService = accessor.get(IClipboardService); + const notificationService = accessor.get(INotificationService); + + if (!(activeEditorPane instanceof MergeEditor)) { + notificationService.info({ + name: localize('mergeEditor.name', 'Merge Editor'), + message: localize('mergeEditor.noActiveMergeEditor', "No active merge editor") + }); + return; + } + const model = activeEditorPane.model; + if (!model) { + return; + } + const contents: MergeEditorContents = { + languageId: model.result.getLanguageId(), + base: model.base.getValue(), + input1: model.input1.getValue(), + input2: model.input2.getValue(), + result: model.result.getValue(), + }; + const jsonStr = JSON.stringify(contents, undefined, 4); + clipboardService.writeText(jsonStr); + + notificationService.info({ + name: localize('mergeEditor.name', 'Merge Editor'), + message: localize('mergeEditor.successfullyCopiedMergeEditorContents', "Successfully copied merge editor contents"), + }); + } +} + +export class MergeEditorOpenContents extends Action2 { + + constructor() { + super({ + id: 'merge.dev.openContents', + category: 'Merge Editor (Dev)', + title: localize('merge.dev.openContents', "Open Contents of Inputs, Base and Result from JSON"), + icon: Codicon.layoutCentered, + f1: true, + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + const service = accessor.get(IWorkbenchFileService); + const instaService = accessor.get(IInstantiationService); + const editorService = accessor.get(IEditorService); + const inputService = accessor.get(IQuickInputService); + const clipboardService = accessor.get(IClipboardService); + const textModelService = accessor.get(ITextModelService); + + const result = await inputService.input({ + prompt: localize('mergeEditor.enterJSON', 'Enter JSON'), + value: await clipboardService.readText(), + }); + if (!result) { + return; + } + + const content: MergeEditorContents = JSON.parse(result); + + const scheme = 'merge-editor-dev'; + + let provider = service.getProvider(scheme) as InMemoryFileSystemProvider | undefined; + if (!provider) { + provider = new InMemoryFileSystemProvider(); + service.registerProvider(scheme, provider); + } + + const baseUri = URI.from({ scheme, path: '/ancestor' }); + const input1Uri = URI.from({ scheme, path: '/input1' }); + const input2Uri = URI.from({ scheme, path: '/input2' }); + const resultUri = URI.from({ scheme, path: '/result' }); + + function writeFile(uri: URI, content: string): Promise<void> { + return provider!.writeFile(uri, VSBuffer.fromString(content).buffer, { create: true, overwrite: true, unlock: true }); + } + + await Promise.all([ + writeFile(baseUri, content.base), + writeFile(input1Uri, content.input1), + writeFile(input2Uri, content.input2), + writeFile(resultUri, content.result), + ]); + + async function setLanguageId(uri: URI, languageId: string): Promise<void> { + const ref = await textModelService.createModelReference(uri); + ref.object.textEditorModel.setMode(languageId); + } + + await Promise.all([ + setLanguageId(baseUri, content.languageId), + setLanguageId(input1Uri, content.languageId), + setLanguageId(input2Uri, content.languageId), + setLanguageId(resultUri, content.languageId), + ]); + + const input = instaService.createInstance( + MergeEditorInput, + baseUri, + { uri: input1Uri, title: 'Input 1', description: 'Input 1', detail: '(from JSON)' }, + { uri: input2Uri, title: 'Input 2', description: 'Input 2', detail: '(from JSON)' }, + resultUri, + ); + editorService.openEditor(input); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts new file mode 100644 index 00000000000..136249c41fa --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextConflict, GoToPreviousConflict, OpenMergeEditor, ToggleActiveConflictInput1, ToggleActiveConflictInput2, SetColumnLayout, SetMixedLayout } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; +import { MergeEditorCopyContentsToJSON, MergeEditorOpenContents } from 'vs/workbench/contrib/mergeEditor/browser/commands/devCommands'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; +import { MergeEditorSerializer } from './mergeEditorSerializer'; + +Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + MergeEditor, + MergeEditor.ID, + localize('name', "Merge Editor") + ), + [ + new SyncDescriptor(MergeEditorInput) + ] +); + +Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer( + MergeEditorInput.ID, + MergeEditorSerializer +); + +registerAction2(SetMixedLayout); +registerAction2(SetColumnLayout); +registerAction2(OpenMergeEditor); + +registerAction2(MergeEditorCopyContentsToJSON); +registerAction2(MergeEditorOpenContents); + +registerAction2(GoToNextConflict); +registerAction2(GoToPreviousConflict); + +registerAction2(ToggleActiveConflictInput1); +registerAction2(ToggleActiveConflictInput2); + +registerAction2(CompareInput1WithBaseCommand); +registerAction2(CompareInput2WithBaseCommand); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts new file mode 100644 index 00000000000..87aa4ac9b0c --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -0,0 +1,239 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; +import Severity from 'vs/base/common/severity'; +import { URI } from 'vs/base/common/uri'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; +import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer'; +import { autorun } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; + +export class MergeEditorInputData { + constructor( + readonly uri: URI, + readonly title: string | undefined, + readonly detail: string | undefined, + readonly description: string | undefined, + ) { } +} + +export class MergeEditorInput extends AbstractTextResourceEditorInput implements ILanguageSupport { + + static readonly ID = 'mergeEditor.Input'; + + private _model?: MergeEditorModel; + private _outTextModel?: ITextFileEditorModel; + private _ignoreUnhandledConflictsForDirtyState?: true; + + constructor( + public readonly base: URI, + public readonly input1: MergeEditorInputData, + public readonly input2: MergeEditorInputData, + public readonly result: URI, + @IInstantiationService private readonly _instaService: IInstantiationService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IDialogService private readonly _dialogService: IDialogService, + @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, + @IEditorService editorService: IEditorService, + @ITextFileService textFileService: ITextFileService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService + ) { + super(result, undefined, editorService, textFileService, labelService, fileService); + + const modelListener = new DisposableStore(); + const handleDidCreate = (model: ITextFileEditorModel) => { + // TODO@jrieken copied from fileEditorInput.ts + if (isEqual(result, model.resource)) { + modelListener.clear(); + this._outTextModel = model; + modelListener.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + modelListener.add(model.onDidSaveError(() => this._onDidChangeDirty.fire())); + + modelListener.add(model.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); + + modelListener.add(model.onWillDispose(() => { + this._outTextModel = undefined; + modelListener.clear(); + })); + } + }; + textFileService.files.onDidCreate(handleDidCreate, this, modelListener); + textFileService.files.models.forEach(handleDidCreate); + this._store.add(modelListener); + } + + override dispose(): void { + super.dispose(); + } + + get typeId(): string { + return MergeEditorInput.ID; + } + + override getName(): string { + return localize('name', "Merging: {0}", super.getName()); + } + + override async resolve(): Promise<MergeEditorModel> { + + if (!this._model) { + + const base = await this._textModelService.createModelReference(this.base); + const input1 = await this._textModelService.createModelReference(this.input1.uri); + const input2 = await this._textModelService.createModelReference(this.input2.uri); + const result = await this._textModelService.createModelReference(this.result); + + this._model = this._instaService.createInstance( + MergeEditorModel, + base.object.textEditorModel, + input1.object.textEditorModel, + this.input1.title, + this.input1.detail, + this.input1.description, + input2.object.textEditorModel, + this.input2.title, + this.input2.detail, + this.input2.description, + result.object.textEditorModel, + this._instaService.createInstance(EditorWorkerServiceDiffComputer), + ); + + await this._model.onInitialized; + + this._store.add(this._model); + this._store.add(base); + this._store.add(input1); + this._store.add(input2); + this._store.add(result); + + this._store.add(autorun(reader => { + this._model?.hasUnhandledConflicts.read(reader); + this._onDidChangeDirty.fire(undefined); + }, 'drive::onDidChangeDirty')); + } + + this._ignoreUnhandledConflictsForDirtyState = undefined; + return this._model; + } + + override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { + if (!(otherInput instanceof MergeEditorInput)) { + return false; + } + return isEqual(this.base, otherInput.base) + && isEqual(this.input1.uri, otherInput.input1.uri) + && isEqual(this.input2.uri, otherInput.input2.uri) + && isEqual(this.result, otherInput.result); + } + + // ---- FileEditorInput + + override isDirty(): boolean { + const textModelDirty = Boolean(this._outTextModel?.isDirty()); + if (textModelDirty) { + // text model dirty -> 3wm is dirty + return true; + } + if (!this._ignoreUnhandledConflictsForDirtyState) { + // unhandled conflicts -> 3wm is dirty UNLESS we explicitly set this input + // to ignore unhandled conflicts for the dirty-state. This happens only + // after confirming to ignore unhandled changes + return Boolean(this._model && this._model.hasUnhandledConflicts.get()); + } + return false; + } + + override async confirm(editors?: ReadonlyArray<IEditorIdentifier>): Promise<ConfirmResult> { + + const inputs: MergeEditorInput[] = [this]; + if (editors) { + for (const { editor } of editors) { + if (editor instanceof MergeEditorInput) { + inputs.push(editor); + } + } + } + + const inputsWithUnhandledConflicts = inputs + .filter(input => input._model && input._model.hasUnhandledConflicts.get()); + + if (inputsWithUnhandledConflicts.length === 0) { + return ConfirmResult.SAVE; + } + + const actions: string[] = []; + const options = { + cancelId: 0, + detail: inputs.length > 1 + ? localize('unhandledConflicts.detailN', 'Merge conflicts in {0} editors will remain unhandled.', inputs.length) + : localize('unhandledConflicts.detail1', 'Merge conflicts in this editor will remain unhandled.') + }; + + const isAnyAutoSave = this._filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF; + if (!isAnyAutoSave) { + // manual-save: FYI and discard + actions.push( + localize('unhandledConflicts.manualSaveIgnore', "Save and Continue with Conflicts"), // 0 + localize('unhandledConflicts.manualSaveNoSave', "Don't Save") // 1 + ); + + } else { + // auto-save: only FYI + actions.push( + localize('unhandledConflicts.ignore', "Continue with Conflicts"), // 0 + ); + } + + actions.push(localize('unhandledConflicts.cancel', "Cancel")); + options.cancelId = actions.length - 1; + + const { choice } = await this._dialogService.show( + Severity.Info, + localize('unhandledConflicts.msg', 'Do you want to continue with unhandled conflicts?'), // 1 + actions, + options + ); + + if (choice === options.cancelId) { + // cancel: stay in editor + return ConfirmResult.CANCEL; + } + + // save or revert: in both cases we tell the inputs to ignore unhandled conflicts + // for the dirty state computation. + for (const input of inputs) { + input._ignoreUnhandledConflictsForDirtyState = true; + } + + if (choice === 0) { + // conflicts: continue with remaining conflicts + return ConfirmResult.SAVE; + } + + // don't save + return ConfirmResult.DONT_SAVE; + } + + setLanguageId(languageId: string, _setExplicitly?: boolean): void { + this._model?.setLanguageId(languageId); + } + + // implement get/set languageId + // implement get/set encoding +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts new file mode 100644 index 00000000000..ee771c4ea8b --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorSerializer.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import { parse } from 'vs/base/common/marshalling'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorSerializer } from 'vs/workbench/common/editor'; +import { MergeEditorInput, MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; + +export class MergeEditorSerializer implements IEditorSerializer { + canSerialize(): boolean { + return true; + } + + serialize(editor: MergeEditorInput): string { + return JSON.stringify(this.toJSON(editor)); + } + + toJSON(editor: MergeEditorInput): MergeEditorInputJSON { + return { + base: editor.base, + input1: editor.input1, + input2: editor.input2, + result: editor.result, + }; + } + + deserialize(instantiationService: IInstantiationService, raw: string): MergeEditorInput | undefined { + try { + const data = <MergeEditorInputJSON>parse(raw); + return instantiationService.createInstance( + MergeEditorInput, + data.base, + new MergeEditorInputData(data.input1.uri, data.input1.title, data.input1.detail, data.input1.description), + new MergeEditorInputData(data.input2.uri, data.input2.title, data.input2.detail, data.input2.description), + data.result + ); + } catch (err) { + onUnexpectedError(err); + return undefined; + } + } +} + +interface MergeEditorInputJSON { + base: URI; + input1: { uri: URI; title?: string; detail?: string; description?: string }; + input2: { uri: URI; title?: string; detail?: string; description?: string }; + result: URI; +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts new file mode 100644 index 00000000000..06401136b7a --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isDefined } from 'vs/base/common/types'; +import { Range } from 'vs/editor/common/core/range'; +import { ICharChange, IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/diffComputer'; +import { ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { DetailedLineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; + +export interface IDiffComputer { + computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<IDiffComputerResult>; +} + +export interface IDiffComputerResult { + diffs: DetailedLineRangeMapping[] | null; +} + +export class EditorWorkerServiceDiffComputer implements IDiffComputer { + constructor(@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService) { } + + async computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<IDiffComputerResult> { + const diffs = await this.editorWorkerService.computeDiff(textModel1.uri, textModel2.uri, false, 1000); + if (!diffs) { + return { diffs: null }; + } + return { diffs: EditorWorkerServiceDiffComputer.fromDiffComputationResult(diffs, textModel1, textModel2) }; + } + + public static fromDiffComputationResult(result: IDiffComputationResult, textModel1: ITextModel, textModel2: ITextModel): DetailedLineRangeMapping[] { + return result.changes.map((c) => fromLineChange(c, textModel1, textModel2)); + } +} + +function fromLineChange(lineChange: ILineChange, originalTextModel: ITextModel, modifiedTextModel: ITextModel): DetailedLineRangeMapping { + let originalRange: LineRange; + if (lineChange.originalEndLineNumber === 0) { + // Insertion + originalRange = new LineRange(lineChange.originalStartLineNumber + 1, 0); + } else { + originalRange = new LineRange(lineChange.originalStartLineNumber, lineChange.originalEndLineNumber - lineChange.originalStartLineNumber + 1); + } + + let modifiedRange: LineRange; + if (lineChange.modifiedEndLineNumber === 0) { + // Deletion + modifiedRange = new LineRange(lineChange.modifiedStartLineNumber + 1, 0); + } else { + modifiedRange = new LineRange(lineChange.modifiedStartLineNumber, lineChange.modifiedEndLineNumber - lineChange.modifiedStartLineNumber + 1); + } + + let innerDiffs = lineChange.charChanges?.map(c => rangeMappingFromCharChange(c, originalTextModel, modifiedTextModel)).filter(isDefined); + if (!innerDiffs || innerDiffs.length === 0) { + innerDiffs = [rangeMappingFromLineRanges(originalRange, modifiedRange)]; + } + + return new DetailedLineRangeMapping( + originalRange, + originalTextModel, + modifiedRange, + modifiedTextModel, + innerDiffs + ); +} + +function rangeMappingFromLineRanges(originalRange: LineRange, modifiedRange: LineRange): RangeMapping { + return new RangeMapping( + new Range( + originalRange.startLineNumber, + 1, + originalRange.endLineNumberExclusive, + 1, + ), + new Range( + modifiedRange.startLineNumber, + 1, + modifiedRange.endLineNumberExclusive, + 1, + ) + ); +} + +function rangeMappingFromCharChange(charChange: ICharChange, inputTextModel: ITextModel, modifiedTextModel: ITextModel): RangeMapping | undefined { + return normalizeRangeMapping(new RangeMapping( + new Range(charChange.originalStartLineNumber, charChange.originalStartColumn, charChange.originalEndLineNumber, charChange.originalEndColumn), + new Range(charChange.modifiedStartLineNumber, charChange.modifiedStartColumn, charChange.modifiedEndLineNumber, charChange.modifiedEndColumn) + ), inputTextModel, modifiedTextModel); +} + +function normalizeRangeMapping(rangeMapping: RangeMapping, inputTextModel: ITextModel, outputTextModel: ITextModel): RangeMapping | undefined { + const inputRangeEmpty = rangeMapping.inputRange.isEmpty(); + const outputRangeEmpty = rangeMapping.outputRange.isEmpty(); + + if (inputRangeEmpty && outputRangeEmpty) { + return undefined; + } + + const originalStartsAtEndOfLine = isAtEndOfLine(rangeMapping.inputRange.startLineNumber, rangeMapping.inputRange.startColumn, inputTextModel); + const modifiedStartsAtEndOfLine = isAtEndOfLine(rangeMapping.outputRange.startLineNumber, rangeMapping.outputRange.startColumn, outputTextModel); + + if (!inputRangeEmpty && !outputRangeEmpty && originalStartsAtEndOfLine && modifiedStartsAtEndOfLine) { + // a b c [\n] x y z \n + // d e f [\n a] \n + // -> + // a b c \n [] x y z \n + // d e f \n [a] \n + + return new RangeMapping( + rangeMapping.inputRange.setStartPosition(rangeMapping.inputRange.startLineNumber + 1, 1), + + rangeMapping.outputRange.setStartPosition(rangeMapping.outputRange.startLineNumber + 1, 1), + ); + } + + if ( + modifiedStartsAtEndOfLine && + originalStartsAtEndOfLine && + ((inputRangeEmpty && rangeEndsAtEndOfLine(rangeMapping.outputRange, outputTextModel)) || + (outputRangeEmpty && rangeEndsAtEndOfLine(rangeMapping.inputRange, inputTextModel))) + ) { + // o: a b c [] \n x y z \n + // m: d e f [\n a] \n + // -> + // o: a b c \n [] x y z \n + // m: d e f \n [a \n] + + // or + + // a b c [\n x y z] \n + // d e f [] \n a \n + // -> + // a b c \n [x y z \n] + // d e f \n [] a \n + + return new RangeMapping( + moveRange(rangeMapping.inputRange), + moveRange(rangeMapping.outputRange) + ); + } + + return rangeMapping; +} + +function isAtEndOfLine(lineNumber: number, column: number, model: ITextModel): boolean { + return column >= model.getLineMaxColumn(lineNumber); +} + +function rangeEndsAtEndOfLine(range: Range, model: ITextModel,): boolean { + return isAtEndOfLine(range.endLineNumber, range.endColumn, model); +} + +function moveRange(range: Range): Range { + return new Range( + range.startLineNumber + 1, + 1, + range.endLineNumber + 1, + 1, + ); +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts new file mode 100644 index 00000000000..de2e9e2c6df --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals } from 'vs/base/common/arrays'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { LineRange } from './lineRange'; + +/** + * Represents an edit, expressed in whole lines: + * At (before) {@link LineRange.startLineNumber}, delete {@link LineRange.lineCount} many lines and insert {@link newLines}. +*/ +export class LineRangeEdit { + constructor( + public readonly range: LineRange, + public readonly newLines: string[] + ) { } + + public equals(other: LineRangeEdit): boolean { + return this.range.equals(other.range) && equals(this.newLines, other.newLines); + } + + public apply(model: ITextModel): void { + new LineEdits([this]).apply(model); + } +} + +export class RangeEdit { + constructor( + public readonly range: Range, + public readonly newText: string + ) { } + + public equals(other: RangeEdit): boolean { + return Range.equalsRange(this.range, other.range) && this.newText === other.newText; + } +} + +export class LineEdits { + constructor(public readonly edits: readonly LineRangeEdit[]) { } + + public apply(model: ITextModel): void { + model.pushEditOperations( + null, + this.edits.map((e) => { + if (e.range.endLineNumberExclusive <= model.getLineCount()) { + return { + range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1), + text: e.newLines.map(s => s + '\n').join(''), + }; + } + + if (e.range.startLineNumber === 1) { + return { + range: new Range(1, 1, model.getLineCount(), Number.MAX_SAFE_INTEGER), + text: e.newLines.join('\n'), + }; + } + + return { + range: new Range(e.range.startLineNumber - 1, Number.MAX_SAFE_INTEGER, model.getLineCount(), Number.MAX_SAFE_INTEGER), + text: e.newLines.map(s => '\n' + s).join(''), + }; + }), + () => null + ); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/lineRange.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/lineRange.ts new file mode 100644 index 00000000000..f78b2b64ba9 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/lineRange.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Comparator, compareBy, numberComparator } from 'vs/base/common/arrays'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Constants } from 'vs/base/common/uint'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; + +export class LineRange { + public static readonly compareByStart: Comparator<LineRange> = compareBy(l => l.startLineNumber, numberComparator); + + public static join(ranges: LineRange[]): LineRange | undefined { + if (ranges.length === 0) { + return undefined; + } + + let startLineNumber = Number.MAX_SAFE_INTEGER; + let endLineNumber = 0; + for (const range of ranges) { + startLineNumber = Math.min(startLineNumber, range.startLineNumber); + endLineNumber = Math.max(endLineNumber, range.startLineNumber + range.lineCount); + } + return new LineRange(startLineNumber, endLineNumber - startLineNumber); + } + + static fromLineNumbers(startLineNumber: number, endExclusiveLineNumber: number): LineRange { + return new LineRange(startLineNumber, endExclusiveLineNumber - startLineNumber); + } + + constructor( + public readonly startLineNumber: number, + public readonly lineCount: number + ) { + if (lineCount < 0) { + throw new BugIndicatingError(); + } + } + + public join(other: LineRange): LineRange { + return new LineRange(Math.min(this.startLineNumber, other.startLineNumber), Math.max(this.endLineNumberExclusive, other.endLineNumberExclusive) - this.startLineNumber); + } + + public get endLineNumberExclusive(): number { + return this.startLineNumber + this.lineCount; + } + + public get isEmpty(): boolean { + return this.lineCount === 0; + } + + /** + * Returns false if there is at least one line between `this` and `other`. + */ + public touches(other: LineRange): boolean { + return ( + this.endLineNumberExclusive >= other.startLineNumber && + other.endLineNumberExclusive >= this.startLineNumber + ); + } + + public isAfter(modifiedRange: LineRange): boolean { + return this.startLineNumber >= modifiedRange.endLineNumberExclusive; + } + + public delta(lineDelta: number): LineRange { + return new LineRange(this.startLineNumber + lineDelta, this.lineCount); + } + + public toString() { + return `[${this.startLineNumber},${this.endLineNumberExclusive})`; + } + + public equals(originalRange: LineRange) { + return this.startLineNumber === originalRange.startLineNumber && this.lineCount === originalRange.lineCount; + } + + public contains(lineNumber: number): boolean { + return this.startLineNumber <= lineNumber && lineNumber < this.endLineNumberExclusive; + } + + public deltaEnd(delta: number): LineRange { + return new LineRange(this.startLineNumber, this.lineCount + delta); + } + + public deltaStart(lineDelta: number): LineRange { + return new LineRange(this.startLineNumber + lineDelta, this.lineCount - lineDelta); + } + + public getLines(model: ITextModel): string[] { + const result = new Array(this.lineCount); + for (let i = 0; i < this.lineCount; i++) { + result[i] = model.getLineContent(this.startLineNumber + i); + } + return result; + } + + public containsRange(range: LineRange): boolean { + return this.startLineNumber <= range.startLineNumber && range.endLineNumberExclusive <= this.endLineNumberExclusive; + } + + public toRange(): Range { + return new Range(this.startLineNumber, 1, this.endLineNumberExclusive, 1); + } + + public toInclusiveRange(): Range | undefined { + if (this.isEmpty) { + return undefined; + } + return new Range(this.startLineNumber, 1, this.endLineNumberExclusive - 1, Constants.MAX_SAFE_SMALL_INTEGER); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mapping.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mapping.ts new file mode 100644 index 00000000000..cfbd5cc48ee --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mapping.ts @@ -0,0 +1,268 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { compareBy, findLast, numberComparator } from 'vs/base/common/arrays'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { concatArrays } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { LineRange } from './lineRange'; +import { LineRangeEdit } from './editing'; + +export class LineRangeMapping { + public static join(mappings: readonly LineRangeMapping[]): LineRangeMapping | undefined { + return mappings.reduce<undefined | LineRangeMapping>((acc, cur) => acc ? acc.join(cur) : cur, undefined); + } + + constructor( + public readonly inputRange: LineRange, + public readonly outputRange: LineRange + ) { } + + public extendInputRange(extendedInputRange: LineRange): LineRangeMapping { + if (!extendedInputRange.containsRange(this.inputRange)) { + throw new BugIndicatingError(); + } + + const startDelta = extendedInputRange.startLineNumber - this.inputRange.startLineNumber; + const endDelta = extendedInputRange.endLineNumberExclusive - this.inputRange.endLineNumberExclusive; + return new LineRangeMapping( + extendedInputRange, + new LineRange( + this.outputRange.startLineNumber + startDelta, + this.outputRange.lineCount - startDelta + endDelta + ) + ); + } + + public join(other: LineRangeMapping): LineRangeMapping { + return new LineRangeMapping( + this.inputRange.join(other.inputRange), + this.outputRange.join(other.outputRange) + ); + } + + public get resultingDeltaFromOriginalToModified(): number { + return this.outputRange.endLineNumberExclusive - this.inputRange.endLineNumberExclusive; + } + + public toString(): string { + return `${this.inputRange.toString()} -> ${this.outputRange.toString()}`; + } + + public addOutputLineDelta(delta: number): LineRangeMapping { + return new LineRangeMapping( + this.inputRange, + this.outputRange.delta(delta) + ); + } + + public getRange(direction: MappingDirection): LineRange { + return direction === MappingDirection.input ? this.inputRange : this.outputRange; + } +} + +export function getOppositeDirection(direction: MappingDirection): MappingDirection { + return direction === MappingDirection.input ? MappingDirection.output : MappingDirection.input; +} + +export const enum MappingDirection { + input = 0, + output = 1, +} + +export class MappingAlignment<T extends LineRangeMapping> { + public static compute<T extends LineRangeMapping>( + fromBaseToInput1: readonly T[], + fromBaseToInput2: readonly T[] + ): MappingAlignment<T>[] { + const compareByStartLineNumber = compareBy<LineRangeMapping, number>( + (d) => d.inputRange.startLineNumber, + numberComparator + ); + + const combinedDiffs = concatArrays( + fromBaseToInput1.map((diff) => ({ source: 0 as const, diff })), + fromBaseToInput2.map((diff) => ({ source: 1 as const, diff })) + ).sort(compareBy((d) => d.diff, compareByStartLineNumber)); + + const currentDiffs = [new Array<T>(), new Array<T>()]; + const deltaFromBaseToInput = [0, 0]; + + const alignments = new Array<MappingAlignment<T>>(); + + function pushAndReset(baseRange: LineRange) { + const mapping1 = LineRangeMapping.join(currentDiffs[0]) || new LineRangeMapping(baseRange, baseRange.delta(deltaFromBaseToInput[0])); + const mapping2 = LineRangeMapping.join(currentDiffs[1]) || new LineRangeMapping(baseRange, baseRange.delta(deltaFromBaseToInput[1])); + + alignments.push( + new MappingAlignment( + currentInputRange!, + mapping1.extendInputRange(currentInputRange!).outputRange, + currentDiffs[0], + mapping2.extendInputRange(currentInputRange!).outputRange, + currentDiffs[1] + ) + ); + currentDiffs[0] = []; + currentDiffs[1] = []; + } + + let currentInputRange: LineRange | undefined; + + for (const diff of combinedDiffs) { + const range = diff.diff.inputRange; + if (currentInputRange && !currentInputRange.touches(range)) { + pushAndReset(currentInputRange); + currentInputRange = undefined; + } + deltaFromBaseToInput[diff.source] = + diff.diff.resultingDeltaFromOriginalToModified; + currentInputRange = currentInputRange ? currentInputRange.join(range) : range; + currentDiffs[diff.source].push(diff.diff); + } + if (currentInputRange) { + pushAndReset(currentInputRange); + } + + return alignments; + } + + constructor( + public readonly baseRange: LineRange, + public readonly input1Range: LineRange, + public readonly input1LineMappings: T[], + public readonly input2Range: LineRange, + public readonly input2LineMappings: T[], + ) { + } + + toString(): string { + return `${this.input1Range} <- ${this.baseRange} -> ${this.input2Range}`; + } +} + +export class DocumentMapping { + public static betweenOutputs( + inputToOutput1: readonly LineRangeMapping[], + inputToOutput2: readonly LineRangeMapping[], + inputLineCount: number + ): DocumentMapping { + const alignments = MappingAlignment.compute(inputToOutput1, inputToOutput2); + const mappings = alignments.map((m) => new LineRangeMapping(m.input1Range, m.input2Range)); + return new DocumentMapping(mappings, inputLineCount); + } + + public getMappingContaining(lineNumber: number, containingDirection: MappingDirection): LineRangeMapping { + const mapTo = getOppositeDirection(containingDirection); + const lastBefore = findLast(this.lineRangeMappings, r => r.getRange(containingDirection).startLineNumber <= lineNumber); + if (lastBefore) { + if (lastBefore.getRange(containingDirection).contains(lineNumber)) { + return lastBefore; + } + const containingRange = new LineRange(lineNumber, 1); + const mappedRange = new LineRange( + lineNumber + + lastBefore.getRange(mapTo).endLineNumberExclusive - + lastBefore.getRange(containingDirection).endLineNumberExclusive, + 1 + ); + + return containingDirection === MappingDirection.input + ? new LineRangeMapping(containingRange, mappedRange) + : new LineRangeMapping(mappedRange, containingRange); + } + return new LineRangeMapping( + new LineRange(lineNumber, 1), + new LineRange(lineNumber, 1) + ); + } + + constructor( + public readonly lineRangeMappings: LineRangeMapping[], + public readonly inputLineCount: number + ) { } +} + +export class DetailedLineRangeMapping extends LineRangeMapping { + public static override join(mappings: readonly DetailedLineRangeMapping[]): DetailedLineRangeMapping | undefined { + return mappings.reduce<undefined | DetailedLineRangeMapping>((acc, cur) => acc ? acc.join(cur) : cur, undefined); + } + + public readonly rangeMappings: readonly RangeMapping[]; + + constructor( + inputRange: LineRange, + public readonly inputTextModel: ITextModel, + outputRange: LineRange, + public readonly outputTextModel: ITextModel, + rangeMappings?: readonly RangeMapping[], + ) { + super(inputRange, outputRange); + + this.rangeMappings = rangeMappings || [new RangeMapping(this.inputRange.toRange(), this.outputRange.toRange())]; + } + + public override addOutputLineDelta(delta: number): DetailedLineRangeMapping { + return new DetailedLineRangeMapping( + this.inputRange, + this.inputTextModel, + this.outputRange.delta(delta), + this.outputTextModel, + this.rangeMappings.map(d => d.addOutputLineDelta(delta)) + ); + } + + public override join(other: DetailedLineRangeMapping): DetailedLineRangeMapping { + return new DetailedLineRangeMapping( + this.inputRange.join(other.inputRange), + this.inputTextModel, + this.outputRange.join(other.outputRange), + this.outputTextModel, + ); + } + + public getLineEdit(): LineRangeEdit { + return new LineRangeEdit(this.inputRange, this.getOutputLines()); + } + + public getReverseLineEdit(): LineRangeEdit { + return new LineRangeEdit(this.outputRange, this.getInputLines()); + } + + private getOutputLines(): string[] { + return this.outputRange.getLines(this.outputTextModel); + } + + private getInputLines(): string[] { + return this.inputRange.getLines(this.inputTextModel); + } +} + +export class RangeMapping { + constructor(public readonly inputRange: Range, public readonly outputRange: Range) { + } + + toString(): string { + function rangeToString(range: Range) { + // TODO@hediet make this the default Range.toString + return `[${range.startLineNumber}:${range.startColumn}, ${range.endLineNumber}:${range.endColumn})`; + } + + return `${rangeToString(this.inputRange)} -> ${rangeToString(this.outputRange)}`; + } + + addOutputLineDelta(deltaLines: number): RangeMapping { + return new RangeMapping( + this.inputRange, + new Range( + this.outputRange.startLineNumber + deltaLines, + this.outputRange.startColumn, + this.outputRange.endLineNumber + deltaLines, + this.outputRange.endColumn + ) + ); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts new file mode 100644 index 00000000000..d9f61d89baf --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -0,0 +1,339 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CompareResult, equals } from 'vs/base/common/arrays'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { autorunHandleChanges, derivedObservable, IObservable, IReader, ITransaction, keepAlive, ObservableValue, transaction, waitForState } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { IDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { DetailedLineRangeMapping, DocumentMapping, LineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; +import { TextModelDiffChangeReason, TextModelDiffs, TextModelDiffState } from 'vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs'; +import { leftJoin } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { ModifiedBaseRange, ModifiedBaseRangeState } from './modifiedBaseRange'; + +export const enum MergeEditorModelState { + initializing = 1, + upToDate = 2, + updating = 3, +} + +export class MergeEditorModel extends EditorModel { + private readonly input1TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input1, this.diffComputer)); + private readonly input2TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input2, this.diffComputer)); + private readonly resultTextModelDiffs = this._register(new TextModelDiffs(this.base, this.result, this.diffComputer)); + + public readonly state = derivedObservable('state', reader => { + const states = [ + this.input1TextModelDiffs, + this.input2TextModelDiffs, + this.resultTextModelDiffs, + ].map((s) => s.state.read(reader)); + + if (states.some((s) => s === TextModelDiffState.initializing)) { + return MergeEditorModelState.initializing; + } + if (states.some((s) => s === TextModelDiffState.updating)) { + return MergeEditorModelState.updating; + } + return MergeEditorModelState.upToDate; + }); + + public readonly isUpToDate = derivedObservable('isUpdating', reader => this.state.read(reader) === MergeEditorModelState.upToDate); + + public readonly onInitialized = waitForState(this.state, state => state === MergeEditorModelState.upToDate); + + public readonly modifiedBaseRanges = derivedObservable<ModifiedBaseRange[]>('modifiedBaseRanges', (reader) => { + const input1Diffs = this.input1TextModelDiffs.diffs.read(reader); + const input2Diffs = this.input2TextModelDiffs.diffs.read(reader); + + return ModifiedBaseRange.fromDiffs(input1Diffs, input2Diffs, this.base, this.input1, this.input2); + }); + + public readonly input1LinesDiffs = this.input1TextModelDiffs.diffs; + public readonly input2LinesDiffs = this.input2TextModelDiffs.diffs; + public readonly resultDiffs = this.resultTextModelDiffs.diffs; + + private readonly modifiedBaseRangeStateStores = + derivedObservable('modifiedBaseRangeStateStores', reader => { + const map = new Map( + this.modifiedBaseRanges.read(reader).map(s => ([s, new ObservableValue(ModifiedBaseRangeState.default, 'State')])) + ); + return map; + }); + + private readonly modifiedBaseRangeHandlingStateStores = + derivedObservable('modifiedBaseRangeHandlingStateStores', reader => { + const map = new Map( + this.modifiedBaseRanges.read(reader).map(s => ([s, new ObservableValue(false, 'State')])) + ); + return map; + }); + + public readonly unhandledConflictsCount = derivedObservable('unhandledConflictsCount', reader => { + const map = this.modifiedBaseRangeHandlingStateStores.read(reader); + let handledCount = 0; + for (const [_key, value] of map) { + handledCount += value.read(reader) ? 1 : 0; + } + return map.size - handledCount; + }); + + public readonly hasUnhandledConflicts = this.unhandledConflictsCount.map(value => value > 0); + + public readonly input1ResultMapping = derivedObservable('input1ResultMapping', reader => { + const resultDiffs = this.resultDiffs.read(reader); + const modifiedBaseRanges = DocumentMapping.betweenOutputs(this.input1LinesDiffs.read(reader), resultDiffs, this.input1.getLineCount()); + + return new DocumentMapping( + modifiedBaseRanges.lineRangeMappings.map((m) => + m.inputRange.isEmpty || m.outputRange.isEmpty + ? new LineRangeMapping( + m.inputRange.deltaStart(-1), + m.outputRange.deltaStart(-1) + ) + : m + ), + modifiedBaseRanges.inputLineCount + ); + }); + + public readonly input2ResultMapping = derivedObservable('input2ResultMapping', reader => { + const resultDiffs = this.resultDiffs.read(reader); + const modifiedBaseRanges = DocumentMapping.betweenOutputs(this.input2LinesDiffs.read(reader), resultDiffs, this.input2.getLineCount()); + + return new DocumentMapping( + modifiedBaseRanges.lineRangeMappings.map((m) => + m.inputRange.isEmpty || m.outputRange.isEmpty + ? new LineRangeMapping( + m.inputRange.deltaStart(-1), + m.outputRange.deltaStart(-1) + ) + : m + ), + modifiedBaseRanges.inputLineCount + ); + }); + + constructor( + readonly base: ITextModel, + readonly input1: ITextModel, + readonly input1Title: string | undefined, + readonly input1Detail: string | undefined, + readonly input1Description: string | undefined, + readonly input2: ITextModel, + readonly input2Title: string | undefined, + readonly input2Detail: string | undefined, + readonly input2Description: string | undefined, + readonly result: ITextModel, + private readonly diffComputer: IDiffComputer, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService, + ) { + super(); + + this._register(keepAlive(this.modifiedBaseRangeStateStores)); + this._register(keepAlive(this.modifiedBaseRangeHandlingStateStores)); + this._register(keepAlive(this.input1ResultMapping)); + this._register(keepAlive(this.input2ResultMapping)); + + let shouldResetHandlingState = true; + this._register( + autorunHandleChanges( + 'Recompute State', + { + handleChange: (ctx) => { + if (ctx.didChange(this.modifiedBaseRangeHandlingStateStores)) { + shouldResetHandlingState = true; + } + return ctx.didChange(this.resultTextModelDiffs.diffs) + // Ignore non-text changes as we update the state directly + ? ctx.change === TextModelDiffChangeReason.textChange + : true; + }, + }, + (reader) => { + const modifiedBaseRangeHandlingStateStores = this.modifiedBaseRangeHandlingStateStores.read(reader); + if (!this.isUpToDate.read(reader)) { + return; + } + const resultDiffs = this.resultTextModelDiffs.diffs.read(reader); + const stores = this.modifiedBaseRangeStateStores.read(reader); + transaction(tx => { + this.recomputeState(resultDiffs, stores, tx); + if (shouldResetHandlingState) { + shouldResetHandlingState = false; + for (const [range, store] of stores) { + const state = store.get(); + modifiedBaseRangeHandlingStateStores.get(range) + ?.set(!(state.isEmpty || state.conflicting), tx); + } + } + }); + } + ) + ); + + this.onInitialized.then(() => { + this.resetUnknown(); + }); + } + + public getRangeInResult(baseRange: LineRange, reader?: IReader): LineRange { + return this.resultTextModelDiffs.getResultRange(baseRange, reader); + } + + private recomputeState(resultDiffs: DetailedLineRangeMapping[], stores: Map<ModifiedBaseRange, ObservableValue<ModifiedBaseRangeState>>, tx: ITransaction): void { + const baseRangeWithStoreAndTouchingDiffs = leftJoin( + stores, + resultDiffs, + (baseRange, diff) => + baseRange[0].baseRange.touches(diff.inputRange) + ? CompareResult.neitherLessOrGreaterThan + : LineRange.compareByStart( + baseRange[0].baseRange, + diff.inputRange + ) + ); + + for (const row of baseRangeWithStoreAndTouchingDiffs) { + row.left[1].set(this.computeState(row.left[0], row.rights), tx); + } + } + + public resetUnknown(): void { + transaction(tx => { + for (const range of this.modifiedBaseRanges.get()) { + if (this.getState(range).get().conflicting) { + this.setState(range, ModifiedBaseRangeState.default, false, tx); + } + } + }); + } + + public mergeNonConflictingDiffs(): void { + transaction((tx) => { + for (const m of this.modifiedBaseRanges.get()) { + if (m.isConflicting) { + continue; + } + this.setState( + m, + m.input1Diffs.length > 0 + ? ModifiedBaseRangeState.default.withInput1(true) + : ModifiedBaseRangeState.default.withInput2(true), + true, + tx + ); + } + }); + } + + public getState(baseRange: ModifiedBaseRange): IObservable<ModifiedBaseRangeState> { + const existingState = this.modifiedBaseRangeStateStores.get().get(baseRange); + if (!existingState) { + throw new BugIndicatingError('object must be from this instance'); + } + return existingState; + } + + public setState( + baseRange: ModifiedBaseRange, + state: ModifiedBaseRangeState, + markHandled: boolean, + transaction: ITransaction + ): void { + if (!this.isUpToDate.get()) { + throw new BugIndicatingError('Cannot set state while updating'); + } + + const existingState = this.modifiedBaseRangeStateStores.get().get(baseRange); + if (!existingState) { + throw new BugIndicatingError('object must be from this instance'); + } + + const conflictingDiffs = this.resultTextModelDiffs.findTouchingDiffs( + baseRange.baseRange + ); + if (conflictingDiffs) { + this.resultTextModelDiffs.removeDiffs(conflictingDiffs, transaction); + } + + const { edit, effectiveState } = baseRange.getEditForBase(state); + + existingState.set(effectiveState, transaction); + + if (edit) { + this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, transaction); + } + + if (markHandled) { + this.modifiedBaseRangeHandlingStateStores + .get() + .get(baseRange)! + .set(true, transaction); + } + } + + private computeState(baseRange: ModifiedBaseRange, conflictingDiffs: DetailedLineRangeMapping[]): ModifiedBaseRangeState { + if (conflictingDiffs.length === 0) { + return ModifiedBaseRangeState.default; + } + const conflictingEdits = conflictingDiffs.map((d) => d.getLineEdit()); + + function editsAgreeWithDiffs(diffs: readonly DetailedLineRangeMapping[]): boolean { + return equals( + conflictingEdits, + diffs.map((d) => d.getLineEdit()), + (a, b) => a.equals(b) + ); + } + + if (editsAgreeWithDiffs(baseRange.input1Diffs)) { + return ModifiedBaseRangeState.default.withInput1(true); + } + if (editsAgreeWithDiffs(baseRange.input2Diffs)) { + return ModifiedBaseRangeState.default.withInput2(true); + } + + const states = [ + ModifiedBaseRangeState.default.withInput1(true).withInput2(true), + ModifiedBaseRangeState.default.withInput2(true).withInput1(true), + ]; + + for (const s of states) { + const { edit } = baseRange.getEditForBase(s); + if (edit) { + const resultRange = this.resultTextModelDiffs.getResultRange(baseRange.baseRange); + const existingLines = resultRange.getLines(this.result); + + if (equals(edit.newLines, existingLines, (a, b) => a === b)) { + return s; + } + } + } + + return ModifiedBaseRangeState.conflicting; + } + + public isHandled(baseRange: ModifiedBaseRange): IObservable<boolean> { + return this.modifiedBaseRangeHandlingStateStores.get().get(baseRange)!; + } + + public setHandled(baseRange: ModifiedBaseRange, handled: boolean, tx: ITransaction): void { + this.modifiedBaseRangeHandlingStateStores.get().get(baseRange)!.set(handled, tx); + } + + public setLanguageId(languageId: string): void { + const language = this.languageService.createById(languageId); + this.modelService.setMode(this.base, language); + this.modelService.setMode(this.input1, language); + this.modelService.setMode(this.input2, language); + this.modelService.setMode(this.result, language); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts new file mode 100644 index 00000000000..4dadbb6816a --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts @@ -0,0 +1,303 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BugIndicatingError } from 'vs/base/common/errors'; +import { ITextModel } from 'vs/editor/common/model'; +import { DetailedLineRangeMapping, MappingAlignment } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { tieBreakComparators, compareBy, numberComparator } from 'vs/base/common/arrays'; +import { splitLines } from 'vs/base/common/strings'; +import { Constants } from 'vs/base/common/uint'; +import { LineRangeEdit, RangeEdit } from 'vs/workbench/contrib/mergeEditor/browser/model/editing'; +import { concatArrays, elementAtOrUndefined } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; + +/** + * Describes modifications in input 1 and input 2 for a specific range in base. + * + * The UI offers a mechanism to either apply all changes from input 1 or input 2 or both. + * + * Immutable. +*/ +export class ModifiedBaseRange { + public static fromDiffs( + diffs1: readonly DetailedLineRangeMapping[], + diffs2: readonly DetailedLineRangeMapping[], + baseTextModel: ITextModel, + input1TextModel: ITextModel, + input2TextModel: ITextModel + ): ModifiedBaseRange[] { + const alignments = MappingAlignment.compute(diffs1, diffs2); + return alignments.map( + (a) => new ModifiedBaseRange( + a.baseRange, + baseTextModel, + a.input1Range, + input1TextModel, + a.input1LineMappings, + a.input2Range, + input2TextModel, + a.input2LineMappings + ) + ); + } + + public readonly input1CombinedDiff = DetailedLineRangeMapping.join(this.input1Diffs); + public readonly input2CombinedDiff = DetailedLineRangeMapping.join(this.input2Diffs); + + + constructor( + public readonly baseRange: LineRange, + public readonly baseTextModel: ITextModel, + public readonly input1Range: LineRange, + public readonly input1TextModel: ITextModel, + public readonly input1Diffs: readonly DetailedLineRangeMapping[], + public readonly input2Range: LineRange, + public readonly input2TextModel: ITextModel, + public readonly input2Diffs: readonly DetailedLineRangeMapping[] + ) { + if (this.input1Diffs.length === 0 && this.input2Diffs.length === 0) { + throw new BugIndicatingError('must have at least one diff'); + } + } + + public getInputRange(inputNumber: 1 | 2): LineRange { + return inputNumber === 1 ? this.input1Range : this.input2Range; + } + + public getInputCombinedDiff(inputNumber: 1 | 2): DetailedLineRangeMapping | undefined { + return inputNumber === 1 ? this.input1CombinedDiff : this.input2CombinedDiff; + } + + public getInputDiffs(inputNumber: 1 | 2): readonly DetailedLineRangeMapping[] { + return inputNumber === 1 ? this.input1Diffs : this.input2Diffs; + } + + public get isConflicting(): boolean { + return this.input1Diffs.length > 0 && this.input2Diffs.length > 0; + } + + public get canBeCombined(): boolean { + return this.combineInputs(1) !== undefined; + } + + public get isOrderRelevant(): boolean { + const input1 = this.combineInputs(1); + const input2 = this.combineInputs(2); + if (!input1 || !input2) { + return false; + } + return !input1.equals(input2); + } + + public getEditForBase(state: ModifiedBaseRangeState): { edit: LineRangeEdit | undefined; effectiveState: ModifiedBaseRangeState } { + const diffs = concatArrays( + state.input1 && this.input1CombinedDiff ? [{ diff: this.input1CombinedDiff, inputNumber: 1 as const }] : [], + state.input2 && this.input2CombinedDiff ? [{ diff: this.input2CombinedDiff, inputNumber: 2 as const }] : [], + ); + + if (state.input2First) { + diffs.reverse(); + } + + const firstDiff = elementAtOrUndefined(diffs, 0); + const secondDiff = elementAtOrUndefined(diffs, 1); + + if (!firstDiff) { + return { edit: undefined, effectiveState: ModifiedBaseRangeState.default }; + } + if (!secondDiff) { + return { edit: firstDiff.diff.getLineEdit(), effectiveState: ModifiedBaseRangeState.default.withInputValue(firstDiff.inputNumber, true) }; + } + + const result = this.combineInputs(state.input2First ? 2 : 1); + if (result) { + return { edit: result, effectiveState: state }; + } + + return { + edit: secondDiff.diff.getLineEdit(), + effectiveState: ModifiedBaseRangeState.default.withInputValue( + secondDiff.inputNumber, + true + ), + }; + } + + private input1LineRangeEdit: LineRangeEdit | undefined | null = null; + private input2LineRangeEdit: LineRangeEdit | undefined | null = null; + + private combineInputs(firstInput: 1 | 2): LineRangeEdit | undefined { + if (firstInput === 1 && this.input1LineRangeEdit !== null) { + return this.input1LineRangeEdit; + } else if (firstInput === 2 && this.input2LineRangeEdit !== null) { + return this.input2LineRangeEdit; + } + + const combinedDiffs = concatArrays( + this.input1Diffs.flatMap((diffs) => + diffs.rangeMappings.map((diff) => ({ diff, input: 1 as const })) + ), + this.input2Diffs.flatMap((diffs) => + diffs.rangeMappings.map((diff) => ({ diff, input: 2 as const })) + ) + ).sort( + tieBreakComparators( + compareBy((d) => d.diff.inputRange, Range.compareRangesUsingStarts), + compareBy((d) => (d.input === firstInput ? 1 : 2), numberComparator) + ) + ); + + const sortedEdits = combinedDiffs.map(d => { + const sourceTextModel = d.input === 1 ? this.input1TextModel : this.input2TextModel; + return new RangeEdit(d.diff.inputRange, sourceTextModel.getValueInRange(d.diff.outputRange)); + }); + + const result = editsToLineRangeEdit(this.baseRange, sortedEdits, this.baseTextModel); + if (firstInput === 1) { + this.input1LineRangeEdit = result; + } else { + this.input2LineRangeEdit = result; + } + return result; + } +} + +function editsToLineRangeEdit(range: LineRange, sortedEdits: RangeEdit[], textModel: ITextModel): LineRangeEdit | undefined { + let text = ''; + const startsLineBefore = range.startLineNumber > 1; + let currentPosition = startsLineBefore + ? new Position( + range.startLineNumber - 1, + Constants.MAX_SAFE_SMALL_INTEGER + ) + : new Position(range.startLineNumber, 1); + + for (const edit of sortedEdits) { + const diffStart = edit.range.getStartPosition(); + if (!currentPosition.isBeforeOrEqual(diffStart)) { + return undefined; + } + let originalText = textModel.getValueInRange(Range.fromPositions(currentPosition, diffStart)); + if (diffStart.lineNumber > textModel.getLineCount()) { + // assert diffStart.lineNumber === textModel.getLineCount() + 1 + // getValueInRange doesn't include this virtual line break, as the document ends the line before. + // endsLineAfter will be false. + originalText += '\n'; + } + text += originalText; + text += edit.newText; + currentPosition = edit.range.getEndPosition(); + } + + const endsLineAfter = range.endLineNumberExclusive <= textModel.getLineCount(); + const end = endsLineAfter ? new Position( + range.endLineNumberExclusive, + 1 + ) : new Position(range.endLineNumberExclusive - 1, Constants.MAX_SAFE_SMALL_INTEGER); + + const originalText = textModel.getValueInRange( + Range.fromPositions(currentPosition, end) + ); + text += originalText; + + const lines = splitLines(text); + if (startsLineBefore) { + lines.shift(); + } + if (endsLineAfter) { + lines.pop(); + } + return new LineRangeEdit(range, lines); +} + + +export class ModifiedBaseRangeState { + public static readonly default = new ModifiedBaseRangeState(false, false, false, false); + public static readonly conflicting = new ModifiedBaseRangeState(false, false, false, true); + + private constructor( + public readonly input1: boolean, + public readonly input2: boolean, + public readonly input2First: boolean, + public readonly conflicting: boolean + ) { } + + public getInput(inputNumber: 1 | 2): InputState { + if (this.conflicting) { + return InputState.conflicting; + } + if (inputNumber === 1) { + return !this.input1 ? InputState.excluded : this.input2First ? InputState.second : InputState.first; + } else { + return !this.input2 ? InputState.excluded : !this.input2First ? InputState.second : InputState.first; + } + } + + public withInputValue(inputNumber: 1 | 2, value: boolean): ModifiedBaseRangeState { + return inputNumber === 1 ? this.withInput1(value) : this.withInput2(value); + } + + public withInput1(value: boolean): ModifiedBaseRangeState { + return new ModifiedBaseRangeState( + value, + this.input2, + value !== this.input2 ? this.input2 : this.input2First, + false + ); + } + + public withInput2(value: boolean): ModifiedBaseRangeState { + return new ModifiedBaseRangeState( + this.input1, + value, + value !== this.input1 ? value : this.input2First, + false + ); + } + + public swap(): ModifiedBaseRangeState { + return new ModifiedBaseRangeState( + this.input2, + this.input1, + !this.input2First, + this.conflicting + ); + } + + public toggle(inputNumber: 1 | 2): ModifiedBaseRangeState { + if (inputNumber === 1) { + return this.withInput1(!this.input1); + } else { + return this.withInput2(!this.input2); + } + } + + public get isEmpty(): boolean { + return !this.input1 && !this.input2; + } + + public toString(): string { + const arr: ('1' | '2')[] = []; + if (this.input1) { + arr.push('1'); + } + if (this.input2) { + arr.push('2'); + } + if (this.input2First) { + arr.reverse(); + } + return arr.join(','); + } +} + +export const enum InputState { + excluded = 0, + first = 1, + second = 2, + conflicting = 3 +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts new file mode 100644 index 00000000000..c57c0ff51ea --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts @@ -0,0 +1,214 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { compareBy, numberComparator } from 'vs/base/common/arrays'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ITextModel } from 'vs/editor/common/model'; +import { IObservable, IReader, ITransaction, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { DetailedLineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; +import { LineRangeEdit } from 'vs/workbench/contrib/mergeEditor/browser/model/editing'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { IDiffComputer } from './diffComputer'; + +export class TextModelDiffs extends Disposable { + private updateCount = 0; + private readonly _state = new ObservableValue<TextModelDiffState, TextModelDiffChangeReason>(TextModelDiffState.initializing, 'LiveDiffState'); + private readonly _diffs = new ObservableValue<DetailedLineRangeMapping[], TextModelDiffChangeReason>([], 'LiveDiffs'); + + private readonly barrier = new ReentrancyBarrier(); + private isDisposed = false; + + constructor( + private readonly baseTextModel: ITextModel, + private readonly textModel: ITextModel, + private readonly diffComputer: IDiffComputer, + ) { + super(); + + this.update(true); + this._register(baseTextModel.onDidChangeContent(this.barrier.makeExclusive(() => this.update()))); + this._register(textModel.onDidChangeContent(this.barrier.makeExclusive(() => this.update()))); + this._register(toDisposable(() => { + this.isDisposed = true; + })); + } + + public get state(): IObservable<TextModelDiffState, TextModelDiffChangeReason> { + return this._state; + } + + public get diffs(): IObservable<DetailedLineRangeMapping[], TextModelDiffChangeReason> { + return this._diffs; + } + + private async update(initializing: boolean = false): Promise<void> { + this.updateCount++; + const currentUpdateCount = this.updateCount; + + if (this._state.get() === TextModelDiffState.initializing) { + initializing = true; + } + + transaction(tx => { + this._state.set( + initializing ? TextModelDiffState.initializing : TextModelDiffState.updating, + tx, + TextModelDiffChangeReason.other + ); + }); + + const result = await this.diffComputer.computeDiff(this.baseTextModel, this.textModel); + if (this.isDisposed) { + return; + } + + if (currentUpdateCount !== this.updateCount) { + // There is a newer update call + return; + } + + transaction(tx => { + if (result.diffs) { + this._state.set(TextModelDiffState.upToDate, tx, TextModelDiffChangeReason.textChange); + this._diffs.set(result.diffs, tx, TextModelDiffChangeReason.textChange); + } else { + this._state.set(TextModelDiffState.error, tx, TextModelDiffChangeReason.textChange); + } + }); + } + + private ensureUpToDate(): void { + if (this.state.get() !== TextModelDiffState.upToDate) { + throw new BugIndicatingError('Cannot remove diffs when the model is not up to date'); + } + } + + public removeDiffs(diffToRemoves: DetailedLineRangeMapping[], transaction: ITransaction | undefined): void { + this.ensureUpToDate(); + + diffToRemoves.sort(compareBy((d) => d.inputRange.startLineNumber, numberComparator)); + diffToRemoves.reverse(); + + let diffs = this._diffs.get(); + + for (const diffToRemove of diffToRemoves) { + // TODO improve performance + const len = diffs.length; + diffs = diffs.filter((d) => d !== diffToRemove); + if (len === diffs.length) { + throw new BugIndicatingError(); + } + + this.barrier.runExclusivelyOrThrow(() => { + diffToRemove.getReverseLineEdit().apply(this.textModel); + }); + + diffs = diffs.map((d) => + d.outputRange.isAfter(diffToRemove.outputRange) + ? d.addOutputLineDelta(diffToRemove.inputRange.lineCount - diffToRemove.outputRange.lineCount) + : d + ); + } + + this._diffs.set(diffs, transaction, TextModelDiffChangeReason.other); + } + + /** + * Edit must be conflict free. + */ + public applyEditRelativeToOriginal(edit: LineRangeEdit, transaction: ITransaction | undefined): void { + this.ensureUpToDate(); + + const editMapping = new DetailedLineRangeMapping( + edit.range, + this.baseTextModel, + new LineRange(edit.range.startLineNumber, edit.newLines.length), + this.textModel + ); + + let firstAfter = false; + let delta = 0; + const newDiffs = new Array<DetailedLineRangeMapping>(); + for (const diff of this.diffs.get()) { + if (diff.inputRange.touches(edit.range)) { + throw new BugIndicatingError('Edit must be conflict free.'); + } else if (diff.inputRange.isAfter(edit.range)) { + if (!firstAfter) { + firstAfter = true; + newDiffs.push(editMapping.addOutputLineDelta(delta)); + } + + newDiffs.push(diff.addOutputLineDelta(edit.newLines.length - edit.range.lineCount)); + } else { + newDiffs.push(diff); + } + + if (!firstAfter) { + delta += diff.outputRange.lineCount - diff.inputRange.lineCount; + } + } + + if (!firstAfter) { + firstAfter = true; + newDiffs.push(editMapping.addOutputLineDelta(delta)); + } + + this.barrier.runExclusivelyOrThrow(() => { + new LineRangeEdit(edit.range.delta(delta), edit.newLines).apply(this.textModel); + }); + this._diffs.set(newDiffs, transaction, TextModelDiffChangeReason.other); + } + + public findTouchingDiffs(baseRange: LineRange): DetailedLineRangeMapping[] { + return this.diffs.get().filter(d => d.inputRange.touches(baseRange)); + } + + private getResultLine(lineNumber: number, reader?: IReader): number | DetailedLineRangeMapping { + let offset = 0; + const diffs = reader ? this.diffs.read(reader) : this.diffs.get(); + for (const diff of diffs) { + if (diff.inputRange.contains(lineNumber) || diff.inputRange.endLineNumberExclusive === lineNumber) { + return diff; + } else if (diff.inputRange.endLineNumberExclusive < lineNumber) { + offset = diff.resultingDeltaFromOriginalToModified; + } else { + break; + } + } + return lineNumber + offset; + } + + public getResultRange(baseRange: LineRange, reader?: IReader): LineRange { + let start = this.getResultLine(baseRange.startLineNumber, reader); + if (typeof start !== 'number') { + start = start.outputRange.startLineNumber; + } + let endExclusive = this.getResultLine(baseRange.endLineNumberExclusive, reader); + if (typeof endExclusive !== 'number') { + endExclusive = endExclusive.outputRange.endLineNumberExclusive; + } + + return LineRange.fromLineNumbers(start, endExclusive); + } +} + +export const enum TextModelDiffChangeReason { + other = 0, + textChange = 1, +} + +export const enum TextModelDiffState { + initializing = 1, + upToDate = 2, + updating = 3, + error = 4, +} + +export interface ITextModelDiffsState { + state: TextModelDiffState; + diffs: DetailedLineRangeMapping[]; +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts new file mode 100644 index 00000000000..23277aeae95 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CompareResult, ArrayQueue } from 'vs/base/common/arrays'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IObservable, autorun } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { IDisposable } from 'xterm'; + +export class ReentrancyBarrier { + private isActive = false; + + public makeExclusive<TFunction extends Function>(fn: TFunction): TFunction { + return ((...args: any[]) => { + if (this.isActive) { + return; + } + this.isActive = true; + try { + return fn(...args); + } finally { + this.isActive = false; + } + }) as any; + } + + public runExclusively(fn: () => void): void { + if (this.isActive) { + return; + } + this.isActive = true; + try { + fn(); + } finally { + this.isActive = false; + } + } + + public runExclusivelyOrThrow(fn: () => void): void { + if (this.isActive) { + throw new BugIndicatingError(); + } + this.isActive = true; + try { + fn(); + } finally { + this.isActive = false; + } + } +} + +export function setStyle( + element: HTMLElement, + style: { + width?: number | string; + height?: number | string; + left?: number | string; + top?: number | string; + } +): void { + Object.entries(style).forEach(([key, value]) => { + element.style.setProperty(key, toSize(value)); + }); +} + +function toSize(value: number | string): string { + return typeof value === 'number' ? `${value}px` : value; +} + +export function applyObservableDecorations(editor: CodeEditorWidget, decorations: IObservable<IModelDeltaDecoration[]>): IDisposable { + const d = new DisposableStore(); + let decorationIds: string[] = []; + d.add(autorun(reader => { + const d = decorations.read(reader); + editor.changeDecorations(a => { + decorationIds = a.deltaDecorations(decorationIds, d); + }); + }, 'Update Decorations')); + d.add({ + dispose: () => { + editor.changeDecorations(a => { + decorationIds = a.deltaDecorations(decorationIds, []); + }); + } + }); + return d; +} + +export function* leftJoin<TLeft, TRight>( + left: Iterable<TLeft>, + right: readonly TRight[], + compare: (left: TLeft, right: TRight) => CompareResult, +): IterableIterator<{ left: TLeft; rights: TRight[] }> { + const rightQueue = new ArrayQueue(right); + for (const leftElement of left) { + rightQueue.takeWhile(rightElement => CompareResult.isGreaterThan(compare(leftElement, rightElement))); + const equals = rightQueue.takeWhile(rightElement => CompareResult.isNeitherLessOrGreaterThan(compare(leftElement, rightElement))); + yield { left: leftElement, rights: equals || [] }; + } +} + +export function* join<TLeft, TRight>( + left: Iterable<TLeft>, + right: readonly TRight[], + compare: (left: TLeft, right: TRight) => CompareResult, +): IterableIterator<{ left?: TLeft; rights: TRight[] }> { + const rightQueue = new ArrayQueue(right); + for (const leftElement of left) { + const skipped = rightQueue.takeWhile(rightElement => CompareResult.isGreaterThan(compare(leftElement, rightElement))); + if (skipped) { + yield { rights: skipped }; + } + const equals = rightQueue.takeWhile(rightElement => CompareResult.isNeitherLessOrGreaterThan(compare(leftElement, rightElement))); + yield { left: leftElement, rights: equals || [] }; + } +} + +export function concatArrays<TArr extends any[]>(...arrays: TArr): TArr[number][number][] { + return ([] as any[]).concat(...arrays); +} + +export function elementAtOrUndefined<T>(arr: T[], index: number): T | undefined { + return arr[index]; +} + +export function thenIfNotDisposed<T>(promise: Promise<T>, then: () => void): IDisposable { + let disposed = false; + promise.then(() => { + if (disposed) { + return; + } + then(); + }); + return toDisposable(() => { + disposed = true; + }); +} + +export function setFields<T extends {}>(obj: T, fields: Partial<T>): T { + return Object.assign(obj, fields); +} + +export function deepMerge<T extends {}>(source1: T, source2: Partial<T>): T { + const result = {} as T; + for (const key in source1) { + result[key] = source1[key]; + } + for (const key in source2) { + const source2Value = source2[key]; + if (typeof result[key] === 'object' && source2Value && typeof source2Value === 'object') { + result[key] = deepMerge<any>(result[key], source2Value); + } else { + result[key] = source2Value as any; + } + } + return result; +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts new file mode 100644 index 00000000000..ac93d328631 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/colors.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; + +export const diff = registerColor( + 'mergeEditor.change.background', + { dark: '#9bb95533', light: '#9bb95533', hcDark: '#9bb95533', hcLight: '#9bb95533', }, + localize('mergeEditor.change.background', 'The background color for changes.') +); + +export const diffWord = registerColor( + 'mergeEditor.change.word.background', + { dark: '#9bb9551e', light: '#9bb9551e', hcDark: '#9bb9551e', hcLight: '#9bb9551e', }, + localize('mergeEditor.change.word.background', 'The background color for word changes.') +); + +export const conflictBorderUnhandledUnfocused = registerColor( + 'mergeEditor.conflict.unhandledUnfocused.border', + { dark: '#ffa6007a', light: '#ffa6007a', hcDark: '#ffa6007a', hcLight: '#ffa6007a', }, + localize('mergeEditor.conflict.unhandledUnfocused.border', 'The border color of unhandled unfocused conflicts.') +); + +export const conflictBorderUnhandledFocused = registerColor( + 'mergeEditor.conflict.unhandledFocused.border', + { dark: '#ffa600', light: '#ffa600', hcDark: '#ffa600', hcLight: '#ffa600', }, + localize('mergeEditor.conflict.unhandledFocused.border', 'The border color of unhandled focused conflicts.') +); + +export const conflictBorderHandledUnfocused = registerColor( + 'mergeEditor.conflict.handledUnfocused.border', + { dark: '#86868649', light: '#86868649', hcDark: '#86868649', hcLight: '#86868649', }, + localize('mergeEditor.conflict.handledUnfocused.border', 'The border color of handled unfocused conflicts.') +); + +export const conflictBorderHandledFocused = registerColor( + 'mergeEditor.conflict.handledFocused.border', + { dark: '#c1c1c1cc', light: '#c1c1c1cc', hcDark: '#c1c1c1cc', hcLight: '#c1c1c1cc', }, + localize('mergeEditor.conflict.handledFocused.border', 'The border color of handled focused conflicts.') +); + + +export const handledConflictMinimapOverViewRulerColor = registerColor( + 'mergeEditor.conflict.handled.minimapOverViewRuler', + { dark: '#adaca8ee', light: '#adaca8ee', hcDark: '#adaca8ee', hcLight: '#adaca8ee', }, + localize('mergeEditor.conflict.handled.minimapOverViewRuler', 'The foreground color for changes in input 1.') +); + +export const unhandledConflictMinimapOverViewRulerColor = registerColor( + 'mergeEditor.conflict.unhandled.minimapOverViewRuler', + { dark: '#fcba03FF', light: '#fcba03FF', hcDark: '#fcba03FF', hcLight: '#fcba03FF', }, + localize('mergeEditor.conflict.unhandled.minimapOverViewRuler', 'The foreground color for changes in input 1.') +); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts new file mode 100644 index 00000000000..2b208650738 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h } from 'vs/base/browser/dom'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { autorun, IReader, observableFromEvent, ObservableValue } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; + +export class EditorGutter<T extends IGutterItemInfo = IGutterItemInfo> extends Disposable { + private readonly scrollTop = observableFromEvent( + this._editor.onDidScrollChange, + (e) => this._editor.getScrollTop() + ); + private readonly modelAttached = observableFromEvent( + this._editor.onDidChangeModel, + (e) => this._editor.hasModel() + ); + + private readonly changeCounter = new ObservableValue(0, 'counter'); + + constructor( + private readonly _editor: CodeEditorWidget, + private readonly _domNode: HTMLElement, + private readonly itemProvider: IGutterItemProvider<T> + ) { + super(); + this._domNode.className = 'gutter monaco-editor'; + const scrollDecoration = this._domNode.appendChild( + h('div.scroll-decoration', { role: 'presentation', ariaHidden: true, style: { width: '100%' } }) + .root + ); + + this._register(autorun((reader) => { + scrollDecoration.className = this.scrollTop.read(reader) === 0 ? '' : 'scroll-decoration'; + }, 'update scroll decoration')); + + + this._register(autorun((reader) => this.render(reader), 'Render')); + + this._editor.onDidChangeViewZones(e => { + this.changeCounter.set(this.changeCounter.get() + 1, undefined); + }); + + this._editor.onDidContentSizeChange(e => { + this.changeCounter.set(this.changeCounter.get() + 1, undefined); + }); + } + + private readonly views = new Map<string, ManagedGutterItemView>(); + + private render(reader: IReader): void { + if (!this.modelAttached.read(reader)) { + return; + } + this.changeCounter.read(reader); + const scrollTop = this.scrollTop.read(reader); + + const visibleRanges = this._editor.getVisibleRanges(); + const unusedIds = new Set(this.views.keys()); + + if (visibleRanges.length > 0) { + const visibleRange = visibleRanges[0]; + + const visibleRange2 = new LineRange( + visibleRange.startLineNumber, + visibleRange.endLineNumber - visibleRange.startLineNumber + ); + + const gutterItems = this.itemProvider.getIntersectingGutterItems( + visibleRange2, + reader + ); + + const lineHeight = this._editor.getOptions().get(EditorOption.lineHeight); + + for (const gutterItem of gutterItems) { + if (!gutterItem.range.touches(visibleRange2)) { + continue; + } + + unusedIds.delete(gutterItem.id); + let view = this.views.get(gutterItem.id); + if (!view) { + const viewDomNode = document.createElement('div'); + viewDomNode.className = 'gutter-item'; + this._domNode.appendChild(viewDomNode); + const itemView = this.itemProvider.createView( + gutterItem, + viewDomNode + ); + view = new ManagedGutterItemView(itemView, viewDomNode); + this.views.set(gutterItem.id, view); + } else { + view.gutterItemView.update(gutterItem); + } + + const top = + (gutterItem.range.startLineNumber === 1 + ? -lineHeight + : this._editor.getTopForLineNumber( + gutterItem.range.startLineNumber - 1 + )) - + scrollTop + + lineHeight; + + const bottom = ( + gutterItem.range.endLineNumberExclusive <= this._editor.getModel()!.getLineCount() + ? this._editor.getTopForLineNumber(gutterItem.range.endLineNumberExclusive) + : this._editor.getTopForLineNumber(gutterItem.range.endLineNumberExclusive - 1) + lineHeight + ) - scrollTop; + + const height = bottom - top; + + view.domNode.style.top = `${top}px`; + view.domNode.style.height = `${height}px`; + + view.gutterItemView.layout(top, height, 0, -1); + } + } + + for (const id of unusedIds) { + const view = this.views.get(id)!; + view.gutterItemView.dispose(); + this._domNode.removeChild(view.domNode); + this.views.delete(id); + } + } +} + +class ManagedGutterItemView { + constructor( + public readonly gutterItemView: IGutterItemView<any>, + public readonly domNode: HTMLDivElement + ) { } +} + +export interface IGutterItemProvider<TItem extends IGutterItemInfo> { + getIntersectingGutterItems(range: LineRange, reader: IReader): TItem[]; + + createView(item: TItem, target: HTMLElement): IGutterItemView<TItem>; +} + +export interface IGutterItemInfo { + id: string; + range: LineRange; + /* + + // To accommodate view zones: + offsetInPx: number; + additionalHeightInPx: number; + */ +} + +export interface IGutterItemView<T extends IGutterItemInfo> extends IDisposable { + update(item: T): void; + layout(top: number, height: number, viewTop: number, viewHeight: number): void; +} + diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts new file mode 100644 index 00000000000..e9a1b888ce0 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h } from 'vs/base/browser/dom'; +import { IView, IViewSize } from 'vs/base/browser/ui/grid/grid'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { ITextModel } from 'vs/editor/common/model'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; +import { IObservable, observableFromEvent, ObservableValue } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; + +export abstract class CodeEditorView extends Disposable { + private readonly _viewModel = new ObservableValue<undefined | MergeEditorViewModel>(undefined, 'viewModel'); + readonly viewModel: IObservable<undefined | MergeEditorViewModel> = this._viewModel; + readonly model = this._viewModel.map(m => m?.model); + + protected readonly htmlElements = h('div.code-view', [ + h('div.title', { $: 'title' }), + h('div.container', [ + h('div.gutter', { $: 'gutterDiv' }), + h('div', { $: 'editor' }), + ]), + ]); + + private readonly _onDidViewChange = new Emitter<IViewSize | undefined>(); + + public readonly view: IView = { + element: this.htmlElements.root, + minimumWidth: DEFAULT_EDITOR_MIN_DIMENSIONS.width, + maximumWidth: DEFAULT_EDITOR_MAX_DIMENSIONS.width, + minimumHeight: DEFAULT_EDITOR_MIN_DIMENSIONS.height, + maximumHeight: DEFAULT_EDITOR_MAX_DIMENSIONS.height, + onDidChange: this._onDidViewChange.event, + layout: (width: number, height: number, top: number, left: number) => { + setStyle(this.htmlElements.root, { width, height, top, left }); + this.editor.layout({ + width: width - this.htmlElements.gutterDiv.clientWidth, + height: height - this.htmlElements.title.clientHeight, + }); + } + // preferredWidth?: number | undefined; + // preferredHeight?: number | undefined; + // priority?: LayoutPriority | undefined; + // snap?: boolean | undefined; + }; + + private readonly _title = new IconLabel(this.htmlElements.title, { supportIcons: true }); + protected readonly _detail = new IconLabel(this.htmlElements.title, { supportIcons: true }); + + public readonly editor = this.instantiationService.createInstance( + CodeEditorWidget, + this.htmlElements.editor, + {}, + { + contributions: this.getEditorContributions(), + } + ); + + public updateOptions(newOptions: Readonly<IEditorOptions>): void { + this.editor.updateOptions(newOptions); + } + + public readonly isFocused = observableFromEvent( + Event.any(this.editor.onDidBlurEditorWidget, this.editor.onDidFocusEditorWidget), + () => this.editor.hasWidgetFocus() + ); + + public readonly cursorPosition = observableFromEvent( + this.editor.onDidChangeCursorPosition, + () => this.editor.getPosition() + ); + + public readonly cursorLineNumber = this.cursorPosition.map(p => p?.lineNumber); + + constructor( + @IInstantiationService + private readonly instantiationService: IInstantiationService + ) { + super(); + } + + protected getEditorContributions(): IEditorContributionDescription[] | undefined { + return undefined; + } + + public setModel( + viewModel: MergeEditorViewModel, + textModel: ITextModel, + title: string, + description: string | undefined, + detail: string | undefined + ): void { + this.editor.setModel(textModel); + this._title.setLabel(title, description); + this._detail.setLabel('', detail); + + this._viewModel.set(viewModel, undefined); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts new file mode 100644 index 00000000000..1610a9a0b0d --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts @@ -0,0 +1,320 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { noBreakWhitespace } from 'vs/base/common/strings'; +import { isDefined } from 'vs/base/common/types'; +import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; +import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; +import { CodeLensContribution } from 'vs/editor/contrib/codelens/browser/codelensController'; +import { localize } from 'vs/nls'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { attachToggleStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { autorun, derivedObservable, IObservable, ITransaction, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { InputState, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; +import { applyObservableDecorations, setFields } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; +import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../editorGutter'; +import { CodeEditorView } from './codeEditorView'; + +export class InputCodeEditorView extends CodeEditorView { + private readonly decorations = derivedObservable('decorations', reader => { + const viewModel = this.viewModel.read(reader); + if (!viewModel) { + return []; + } + const model = viewModel.model; + + const activeModifiedBaseRange = viewModel.activeModifiedBaseRange.read(reader); + + const result = new Array<IModelDeltaDecoration>(); + for (const modifiedBaseRange of model.modifiedBaseRanges.read(reader)) { + + const range = modifiedBaseRange.getInputRange(this.inputNumber); + if (range && !range.isEmpty) { + const blockClassNames = ['merge-editor-block']; + const isHandled = model.isHandled(modifiedBaseRange).read(reader); + if (isHandled) { + blockClassNames.push('handled'); + } + if (modifiedBaseRange === activeModifiedBaseRange) { + blockClassNames.push('focused'); + } + const inputClassName = this.inputNumber === 1 ? 'input1' : 'input2'; + blockClassNames.push(inputClassName); + + result.push({ + range: range.toInclusiveRange()!, + options: { + isWholeLine: true, + blockClassName: blockClassNames.join(' '), + description: 'Merge Editor', + minimap: { + position: MinimapPosition.Gutter, + color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, + }, + overviewRuler: { + position: OverviewRulerLane.Center, + color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, + } + } + }); + + const inputDiffs = modifiedBaseRange.getInputDiffs(this.inputNumber); + for (const diff of inputDiffs) { + const range = diff.outputRange.toInclusiveRange(); + if (range) { + result.push({ + range, + options: { + className: `merge-editor-diff ${inputClassName}`, + description: 'Merge Editor', + isWholeLine: true, + } + }); + } + + if (diff.rangeMappings) { + for (const d of diff.rangeMappings) { + result.push({ + range: d.outputRange, + options: { + className: `merge-editor-diff-word ${inputClassName}`, + description: 'Merge Editor' + } + }); + } + } + } + } + } + return result; + }); + + constructor( + public readonly inputNumber: 1 | 2, + @IInstantiationService instantiationService: IInstantiationService, + @IContextMenuService contextMenuService: IContextMenuService, + @IThemeService themeService: IThemeService, + ) { + super(instantiationService); + + this._register(applyObservableDecorations(this.editor, this.decorations)); + + this._register( + new EditorGutter(this.editor, this.htmlElements.gutterDiv, { + getIntersectingGutterItems: (range, reader) => { + const viewModel = this.viewModel.read(reader); + if (!viewModel) { return []; } + const model = viewModel.model; + + return model.modifiedBaseRanges.read(reader) + .filter((r) => r.getInputDiffs(this.inputNumber).length > 0) + .map<ModifiedBaseRangeGutterItemInfo>((baseRange, idx) => ({ + id: idx.toString(), + range: baseRange.getInputRange(this.inputNumber), + enabled: model.isUpToDate, + toggleState: derivedObservable('toggle', (reader) => { + const input = model + .getState(baseRange) + .read(reader) + .getInput(this.inputNumber); + return input === InputState.second && !baseRange.isOrderRelevant + ? InputState.first + : input; + } + ), + setState: (value, tx) => viewModel.setState( + baseRange, + model + .getState(baseRange) + .get() + .withInputValue(this.inputNumber, value), + tx + ), + getContextMenuActions: () => { + const state = model.getState(baseRange).get(); + const handled = model.isHandled(baseRange).get(); + + const update = (newState: ModifiedBaseRangeState) => { + transaction(tx => viewModel.setState(baseRange, newState, tx)); + }; + + function action(id: string, label: string, targetState: ModifiedBaseRangeState, checked: boolean) { + const action = new Action(id, label, undefined, true, () => { + update(targetState); + }); + action.checked = checked; + return action; + } + const both = state.input1 && state.input2; + + return [ + baseRange.input1Diffs.length > 0 + ? action( + 'mergeEditor.acceptInput1', + localize('mergeEditor.accept', 'Accept {0}', model.input1Title), + state.toggle(1), + state.input1 + ) + : undefined, + baseRange.input2Diffs.length > 0 + ? action( + 'mergeEditor.acceptInput2', + localize('mergeEditor.accept', 'Accept {0}', model.input2Title), + state.toggle(2), + state.input2 + ) + : undefined, + baseRange.isConflicting + ? setFields( + action( + 'mergeEditor.acceptBoth', + localize( + 'mergeEditor.acceptBoth', + 'Accept Both' + ), + state.withInput1(!both).withInput2(!both), + both + ), + { enabled: baseRange.canBeCombined } + ) + : undefined, + new Separator(), + baseRange.isConflicting + ? setFields( + action( + 'mergeEditor.swap', + localize('mergeEditor.swap', 'Swap'), + state.swap(), + false + ), + { enabled: !state.isEmpty && (!both || baseRange.isOrderRelevant) } + ) + : undefined, + + setFields( + new Action( + 'mergeEditor.markAsHandled', + localize('mergeEditor.markAsHandled', 'Mark as Handled'), + undefined, + true, + () => { + transaction((tx) => { + model.setHandled(baseRange, !handled, tx); + }); + } + ), + { checked: handled } + ), + ].filter(isDefined); + } + })); + }, + createView: (item, target) => new MergeConflictGutterItemView(item, target, contextMenuService, themeService), + }) + ); + } + + protected override getEditorContributions(): IEditorContributionDescription[] | undefined { + return EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeLensContribution.ID); + } +} + +export interface ModifiedBaseRangeGutterItemInfo extends IGutterItemInfo { + enabled: IObservable<boolean>; + toggleState: IObservable<InputState>; + setState(value: boolean, tx: ITransaction): void; + getContextMenuActions(): readonly IAction[]; +} + +export class MergeConflictGutterItemView extends Disposable implements IGutterItemView<ModifiedBaseRangeGutterItemInfo> { + private readonly item = new ObservableValue<ModifiedBaseRangeGutterItemInfo | undefined>(undefined, 'item'); + + constructor( + item: ModifiedBaseRangeGutterItemInfo, + private readonly target: HTMLElement, + contextMenuService: IContextMenuService, + themeService: IThemeService + ) { + super(); + + this.item.set(item, undefined); + + target.classList.add('merge-accept-gutter-marker'); + + const checkBox = new Toggle({ isChecked: false, title: localize('accept', "Accept"), icon: Codicon.check }); + + this._register(attachToggleStyler(checkBox, themeService)); + + this._register( + dom.addDisposableListener(checkBox.domNode, dom.EventType.MOUSE_DOWN, (e) => { + if (e.button === 2) { + const item = this.item.get(); + if (!item) { + return; + } + + contextMenuService.showContextMenu({ + getAnchor: () => checkBox.domNode, + getActions: item.getContextMenuActions, + }); + + e.stopPropagation(); + e.preventDefault(); + } + }) + ); + checkBox.domNode.classList.add('accept-conflict-group'); + + this._register( + autorun((reader) => { + const item = this.item.read(reader)!; + const value = item.toggleState.read(reader); + const iconMap: Record<InputState, { icon: Codicon | undefined; checked: boolean }> = { + [InputState.excluded]: { icon: undefined, checked: false }, + [InputState.conflicting]: { icon: Codicon.circleFilled, checked: false }, + [InputState.first]: { icon: Codicon.check, checked: true }, + [InputState.second]: { icon: Codicon.checkAll, checked: true }, + }; + checkBox.setIcon(iconMap[value].icon); + checkBox.checked = iconMap[value].checked; + + if (!item.enabled.read(reader)) { + checkBox.disable(); + } else { + checkBox.enable(); + } + }, 'Update Toggle State') + ); + + this._register(checkBox.onChange(() => { + transaction(tx => { + this.item.get()!.setState(checkBox.checked, tx); + }); + })); + + target.appendChild(dom.h('div.background', [noBreakWhitespace]).root); + target.appendChild( + dom.h('div.checkbox', [dom.h('div.checkbox-background', [checkBox.domNode])]).root + ); + } + + layout(top: number, height: number, viewTop: number, viewHeight: number): void { + this.target.classList.remove('multi-line'); + this.target.classList.remove('single-line'); + this.target.classList.add(height > 30 ? 'multi-line' : 'single-line'); + } + + update(baseRange: ModifiedBaseRangeGutterItemInfo): void { + this.item.set(baseRange, undefined); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts new file mode 100644 index 00000000000..fc6919af08a --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CompareResult } from 'vs/base/common/arrays'; +import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { autorun, derivedObservable } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { applyObservableDecorations, join } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; +import { CodeEditorView } from './codeEditorView'; + +export class ResultCodeEditorView extends CodeEditorView { + private readonly decorations = derivedObservable('decorations', reader => { + const viewModel = this.viewModel.read(reader); + if (!viewModel) { + return []; + } + const model = viewModel.model; + const result = new Array<IModelDeltaDecoration>(); + + const baseRangeWithStoreAndTouchingDiffs = join( + model.modifiedBaseRanges.read(reader), + model.resultDiffs.read(reader), + (baseRange, diff) => baseRange.baseRange.touches(diff.inputRange) + ? CompareResult.neitherLessOrGreaterThan + : LineRange.compareByStart( + baseRange.baseRange, + diff.inputRange + ) + ); + + const activeModifiedBaseRange = viewModel.activeModifiedBaseRange.read(reader); + + for (const m of baseRangeWithStoreAndTouchingDiffs) { + const modifiedBaseRange = m.left; + + if (modifiedBaseRange) { + const range = model.getRangeInResult(modifiedBaseRange.baseRange, reader).toInclusiveRange(); + if (range) { + const blockClassNames = ['merge-editor-block']; + const isHandled = model.isHandled(modifiedBaseRange).read(reader); + if (isHandled) { + blockClassNames.push('handled'); + } + if (modifiedBaseRange === activeModifiedBaseRange) { + blockClassNames.push('focused'); + } + blockClassNames.push('result'); + + result.push({ + range, + options: { + isWholeLine: true, + blockClassName: blockClassNames.join(' '), + description: 'Result Diff', + minimap: { + position: MinimapPosition.Gutter, + color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, + }, + overviewRuler: { + position: OverviewRulerLane.Center, + color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor }, + } + } + }); + } + } + + for (const diff of m.rights) { + const range = diff.outputRange.toInclusiveRange(); + if (range) { + result.push({ + range, + options: { + className: `merge-editor-diff result`, + description: 'Merge Editor', + isWholeLine: true, + } + }); + } + + if (diff.rangeMappings) { + for (const d of diff.rangeMappings) { + result.push({ + range: d.outputRange, + options: { + className: `merge-editor-diff-word result`, + description: 'Merge Editor' + } + }); + } + } + } + } + return result; + }); + + constructor( + @IInstantiationService instantiationService: IInstantiationService + ) { + super(instantiationService); + + this._register(applyObservableDecorations(this.editor, this.decorations)); + + + this._register(autorun(reader => { + const model = this.model.read(reader); + if (!model) { + return; + } + const count = model.unhandledConflictsCount.read(reader); + + this._detail.setLabel(count === 1 + ? localize( + 'mergeEditor.remainingConflicts', + '{0} Remaining Conflict', + count + ) + : localize( + 'mergeEditor.remainingConflict', + '{0} Remaining Conflicts', + count + )); + }, 'update label')); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/view/media/mergeEditor.css new file mode 100644 index 00000000000..b832e88bd84 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/media/mergeEditor.css @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .merge-editor .code-view > .title { + padding: 0 10px; + height: 30px; + display: flex; + align-content: center; + justify-content: space-between; +} + +.monaco-workbench .merge-editor .code-view > .title .monaco-icon-label { + margin: auto 0; +} + +.monaco-workbench .merge-editor .code-view > .container { + display: flex; + flex-direction: row; +} + +.monaco-workbench .merge-editor .code-view > .container > .gutter { + width: 24px; + position: relative; + overflow: hidden; + + flex-shrink: 0; + flex-grow: 0; +} + +.merge-editor-diff { + background-color: var(--vscode-mergeEditor-change-background); +} + +.merge-editor-diff-word { + background-color: var(--vscode-mergeEditor-change-word-background); +} + +.merge-editor-block { + border: 2px solid var(--vscode-mergeEditor-conflict-unhandledUnfocused-border); +} + +.merge-editor-block.focused { + border: 2px solid var(--vscode-mergeEditor-conflict-unhandledFocused-border); +} + +.merge-editor-block.handled { + border: 2px solid var(--vscode-mergeEditor-conflict-handledUnfocused-border); + +} + +.merge-editor-block.handled.focused { + border: 2px solid var(--vscode-mergeEditor-conflict-handledFocused-border); +} + +.gutter-item { + position: absolute; +} + +.merge-accept-gutter-marker { + width: 28px; + margin-left: 4px; +} + +.merge-accept-gutter-marker .background { + height: 100%; + width: 50%; + position: absolute; +} + +.merge-accept-gutter-marker.multi-line .background { + border: 1px solid var(--vscode-checkbox-border); + border-right: 0; + left: 8px; + width: 10px; +} + +.merge-accept-gutter-marker .checkbox { + height: 100%; + width: 100%; + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; +} + +.accept-conflict-group.monaco-custom-toggle { + height: 18px; + width: 18px; + border: 1px solid transparent; + border-radius: 3px; + margin-right: 0px; + margin-left: 0px; + padding: 0px; + opacity: 1; + background-size: 16px !important; + background-color: var(--vscode-checkbox-border); +} + +.merge-accept-gutter-marker .checkbox-background { + background: var(--vscode-editor-background); +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts new file mode 100644 index 00000000000..ae2fcbcd7f1 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -0,0 +1,528 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, Dimension, reset } from 'vs/base/browser/dom'; +import { Direction, Grid, IView, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; +import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview'; +import { IAction } from 'vs/base/common/actions'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Color } from 'vs/base/common/color'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./media/mergeEditor'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { localize } from 'vs/nls'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; +import { AbstractTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { EditorInputWithOptions, EditorResourceAccessor, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; +import { autorunWithStore, IObservable } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { DocumentMapping, getOppositeDirection, MappingDirection } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; +import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; +import { deepMerge, ReentrancyBarrier, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; +import { ctxBaseResourceScheme, ctxIsMergeEditor, ctxMergeEditorLayout, MergeEditorLayoutTypes } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import './colors'; +import { InputCodeEditorView } from './editors/inputCodeEditorView'; +import { ResultCodeEditorView } from './editors/resultCodeEditorView'; + +class MergeEditorLayout { + + private static readonly _key = 'mergeEditor/layout'; + private _value: MergeEditorLayoutTypes = 'mixed'; + + + constructor(@IStorageService private _storageService: IStorageService) { + const value = _storageService.get(MergeEditorLayout._key, StorageScope.PROFILE, 'mixed'); + if (value === 'mixed' || value === 'columns') { + this._value = value; + } else { + this._value = 'mixed'; + } + } + + get value() { + return this._value; + } + + set value(value) { + if (this._value !== value) { + this._value = value; + this._storageService.store(MergeEditorLayout._key, this._value, StorageScope.PROFILE, StorageTarget.USER); + } + } +} + +export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> { + + static readonly ID = 'mergeEditor'; + + private readonly _sessionDisposables = new DisposableStore(); + + private _grid!: Grid<IView>; + + + private readonly input1View = this._register(this.instantiation.createInstance(InputCodeEditorView, 1)); + private readonly input2View = this._register(this.instantiation.createInstance(InputCodeEditorView, 2)); + private readonly inputResultView = this._register(this.instantiation.createInstance(ResultCodeEditorView)); + + private readonly _layoutMode: MergeEditorLayout; + private readonly _ctxIsMergeEditor: IContextKey<boolean>; + private readonly _ctxUsesColumnLayout: IContextKey<string>; + private readonly _ctxBaseResourceScheme: IContextKey<string>; + + private _model: MergeEditorModel | undefined; + public get model(): MergeEditorModel | undefined { return this._model; } + + private get inputsWritable(): boolean { + return !!this._configurationService.getValue<boolean>('mergeEditor.writableInputs'); + } + + constructor( + @IInstantiationService private readonly instantiation: IInstantiationService, + @ILabelService private readonly _labelService: ILabelService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IThemeService themeService: IThemeService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IEditorService editorService: IEditorService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IFileService fileService: IFileService, + @IEditorResolverService private readonly _editorResolverService: IEditorResolverService, + ) { + super(MergeEditor.ID, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + + this._ctxIsMergeEditor = ctxIsMergeEditor.bindTo(_contextKeyService); + this._ctxUsesColumnLayout = ctxMergeEditorLayout.bindTo(_contextKeyService); + this._ctxBaseResourceScheme = ctxBaseResourceScheme.bindTo(_contextKeyService); + + this._layoutMode = instantiation.createInstance(MergeEditorLayout); + this._ctxUsesColumnLayout.set(this._layoutMode.value); + + const reentrancyBarrier = new ReentrancyBarrier(); + + this._store.add( + this.input1View.editor.onDidScrollChange( + reentrancyBarrier.makeExclusive((c) => { + if (c.scrollTopChanged) { + const mapping = this.model?.input1ResultMapping.get(); + synchronizeScrolling(this.input1View.editor, this.inputResultView.editor, mapping, MappingDirection.input); + this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + } + if (c.scrollLeftChanged) { + this.input2View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); + this.inputResultView.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); + } + }) + ) + ); + this._store.add( + this.input2View.editor.onDidScrollChange( + reentrancyBarrier.makeExclusive((c) => { + if (c.scrollTopChanged) { + const mapping = this.model?.input2ResultMapping.get(); + synchronizeScrolling(this.input2View.editor, this.inputResultView.editor, mapping, MappingDirection.input); + this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate); + } + if (c.scrollLeftChanged) { + this.input1View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); + this.inputResultView.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); + } + }) + ) + ); + this._store.add( + this.inputResultView.editor.onDidScrollChange( + reentrancyBarrier.makeExclusive((c) => { + if (c.scrollTopChanged) { + const mapping1 = this.model?.input1ResultMapping.get(); + synchronizeScrolling(this.inputResultView.editor, this.input1View.editor, mapping1, MappingDirection.output); + const mapping2 = this.model?.input2ResultMapping.get(); + synchronizeScrolling(this.inputResultView.editor, this.input2View.editor, mapping2, MappingDirection.output); + } + if (c.scrollLeftChanged) { + this.input1View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); + this.input2View.editor.setScrollLeft(c.scrollLeft, ScrollType.Immediate); + } + }) + ) + ); + + + // TODO@jrieken make this proper: add menu id and allow extensions to contribute + const toolbarMenu = this._menuService.createMenu(MenuId.MergeToolbar, this._contextKeyService); + const toolbarMenuDisposables = new DisposableStore(); + const toolbarMenuRender = () => { + toolbarMenuDisposables.clear(); + + const actions: IAction[] = []; + createAndFillInActionBarActions(toolbarMenu, { renderShortTitle: true, shouldForwardArgs: true }, actions); + if (actions.length > 0) { + const [first] = actions; + const acceptBtn = this.instantiation.createInstance(FloatingClickWidget, this.inputResultView.editor, first.label, first.id); + toolbarMenuDisposables.add(acceptBtn.onClick(() => first.run(this.inputResultView.editor.getModel()?.uri))); + toolbarMenuDisposables.add(acceptBtn); + acceptBtn.render(); + } + }; + this._store.add(toolbarMenu); + this._store.add(toolbarMenuDisposables); + this._store.add(toolbarMenu.onDidChange(toolbarMenuRender)); + toolbarMenuRender(); + } + + public get viewModel(): IObservable<MergeEditorViewModel | undefined> { + return this.input1View.viewModel; + } + + override dispose(): void { + this._sessionDisposables.dispose(); + this._ctxIsMergeEditor.reset(); + super.dispose(); + } + + override getTitle(): string { + if (this.input) { + return this.input.getName(); + } + + return localize('mergeEditor', "Text Merge Editor"); + } + + protected createEditorControl(parent: HTMLElement, initialOptions: ICodeEditorOptions): void { + parent.classList.add('merge-editor'); + + this._grid = SerializableGrid.from<any /*TODO@jrieken*/>({ + orientation: Orientation.VERTICAL, + size: 100, + groups: [ + { + size: 38, + groups: [{ + data: this.input1View.view + }, { + data: this.input2View.view + }] + }, + { + size: 62, + data: this.inputResultView.view + }, + ] + }, { + styles: { separatorBorder: this.theme.getColor(settingsSashBorder) ?? Color.transparent }, + proportionalLayout: true + }); + + reset(parent, this._grid.element); + this._register(this._grid); + + if (this._layoutMode.value === 'columns') { + this._grid.moveView(this.inputResultView.view, Sizing.Distribute, this.input1View.view, Direction.Right); + } + + this.applyOptions(initialOptions); + } + + protected updateEditorControlOptions(options: ICodeEditorOptions): void { + this.applyOptions(options); + } + + private applyOptions(options: ICodeEditorOptions): void { + const inputOptions: ICodeEditorOptions = deepMerge<ICodeEditorOptions>(options, { + minimap: { enabled: false }, + glyphMargin: false, + lineNumbersMinChars: 2, + readOnly: !this.inputsWritable + }); + + this.input1View.updateOptions(inputOptions); + this.input2View.updateOptions(inputOptions); + this.inputResultView.updateOptions(options); + } + + protected getMainControl(): ICodeEditor | undefined { + return this.inputResultView.editor; + } + + layout(dimension: Dimension): void { + this._grid.layout(dimension.width, dimension.height); + } + + override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { + if (!(input instanceof MergeEditorInput)) { + throw new BugIndicatingError('ONLY MergeEditorInput is supported'); + } + await super.setInput(input, options, context, token); + + this._sessionDisposables.clear(); + this._toggleEditorOverwrite(true); + + const model = await input.resolve(); + this._model = model; + + const viewModel = new MergeEditorViewModel(model, this.input1View, this.input2View, this.inputResultView); + + this.input1View.setModel(viewModel, model.input1, model.input1Title || localize('input1', 'Input 1'), model.input1Detail, model.input1Description); + this.input2View.setModel(viewModel, model.input2, model.input2Title || localize('input2', 'Input 2',), model.input2Detail, model.input2Description); + this.inputResultView.setModel(viewModel, model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true }), undefined); + this._ctxBaseResourceScheme.set(model.base.uri.scheme); + + const viewState = this.loadEditorViewState(input, context); + this._applyViewState(viewState); + + this._sessionDisposables.add(thenIfNotDisposed(model.onInitialized, () => { + const firstConflict = model.modifiedBaseRanges.get().find(r => r.isConflicting); + if (!firstConflict) { + return; + } + + this.input1View.editor.revealLineInCenter(firstConflict.input1Range.startLineNumber); + })); + + + this._sessionDisposables.add(autorunWithStore((reader, store) => { + const input1ViewZoneIds: string[] = []; + const input2ViewZoneIds: string[] = []; + for (const m of model.modifiedBaseRanges.read(reader)) { + const max = Math.max(m.input1Range.lineCount, m.input2Range.lineCount, 1); + + this.input1View.editor.changeViewZones(a => { + input1ViewZoneIds.push(a.addZone({ + afterLineNumber: m.input1Range.endLineNumberExclusive - 1, + heightInLines: max - m.input1Range.lineCount, + domNode: $('div.diagonal-fill'), + })); + }); + + this.input2View.editor.changeViewZones(a => { + input2ViewZoneIds.push(a.addZone({ + afterLineNumber: m.input2Range.endLineNumberExclusive - 1, + heightInLines: max - m.input2Range.lineCount, + domNode: $('div.diagonal-fill'), + })); + }); + } + + store.add({ + dispose: () => { + this.input1View.editor.changeViewZones(a => { + for (const zone of input1ViewZoneIds) { + a.removeZone(zone); + } + }); + this.input2View.editor.changeViewZones(a => { + for (const zone of input2ViewZoneIds) { + a.removeZone(zone); + } + }); + } + }); + }, 'update alignment view zones')); + } + + override setOptions(options: ITextEditorOptions | undefined): void { + super.setOptions(options); + + if (options) { + applyTextEditorOptions(options, this.inputResultView.editor, ScrollType.Smooth); + } + } + + override clearInput(): void { + super.clearInput(); + + this._sessionDisposables.clear(); + this._toggleEditorOverwrite(false); + + for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { + editor.setModel(null); + } + } + + override focus(): void { + (this.getControl() ?? this.inputResultView.editor).focus(); + } + + override hasFocus(): boolean { + for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { + if (editor.hasTextFocus()) { + return true; + } + } + return super.hasFocus(); + } + + protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + super.setEditorVisible(visible, group); + + for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { + if (visible) { + editor.onVisible(); + } else { + editor.onHide(); + } + } + + this._ctxIsMergeEditor.set(visible); + this._toggleEditorOverwrite(visible); + } + + private readonly _editorOverrideHandle = this._store.add(new MutableDisposable()); + + private _toggleEditorOverwrite(haveIt: boolean) { + if (!haveIt) { + this._editorOverrideHandle.clear(); + return; + } + // this is RATHER UGLY. I dynamically register an editor for THIS (editor,input) so that + // navigating within the merge editor works, e.g navigating from the outline or breakcrumps + // or revealing a definition, reference etc + // TODO@jrieken @bpasero @lramos15 + const input = this.input; + if (input instanceof MergeEditorInput) { + this._editorOverrideHandle.value = this._editorResolverService.registerEditor( + `${input.result.scheme}:${input.result.fsPath}`, + { + id: `${this.getId()}/fake`, + label: this.input?.getName()!, + priority: RegisteredEditorPriority.exclusive + }, + {}, + (candidate): EditorInputWithOptions => { + const resource = EditorResourceAccessor.getCanonicalUri(candidate); + if (!isEqual(resource, this.model?.result.uri)) { + throw new Error(`Expected to be called WITH ${input.result.toString()}`); + } + return { editor: input }; + } + ); + } + } + + // ---- interact with "outside world" via`getControl`, `scopedContextKeyService`: we only expose the result-editor keep the others internal + + override getControl(): ICodeEditor | undefined { + return this.inputResultView.editor; + } + + override get scopedContextKeyService(): IContextKeyService | undefined { + const control = this.getControl(); + return control?.invokeWithinContext(accessor => accessor.get(IContextKeyService)); + } + + // --- layout + + setLayout(newValue: MergeEditorLayoutTypes): void { + const value = this._layoutMode.value; + if (value === newValue) { + return; + } + if (newValue === 'mixed') { + this._grid.moveView(this.inputResultView.view, this._grid.height * .62, this.input1View.view, Direction.Down); + this._grid.moveView(this.input2View.view, Sizing.Distribute, this.input1View.view, Direction.Right); + } else { + this._grid.moveView(this.inputResultView.view, Sizing.Distribute, this.input1View.view, Direction.Right); + } + this._layoutMode.value = newValue; + this._ctxUsesColumnLayout.set(newValue); + } + + private _applyViewState(state: IMergeEditorViewState | undefined) { + if (!state) { + return; + } + this.inputResultView.editor.restoreViewState(state); + if (state.input1State) { + this.input1View.editor.restoreViewState(state.input1State); + } + if (state.input2State) { + this.input2View.editor.restoreViewState(state.input2State); + } + if (state.focusIndex >= 0) { + [this.input1View.editor, this.input2View.editor, this.inputResultView.editor][state.focusIndex].focus(); + } + } + + protected computeEditorViewState(resource: URI): IMergeEditorViewState | undefined { + if (!isEqual(this.model?.result.uri, resource)) { + // TODO@bpasero Why not check `input#resource` and don't ask me for "forgein" resources? + return undefined; + } + const result = this.inputResultView.editor.saveViewState(); + if (!result) { + return undefined; + } + const input1State = this.input1View.editor.saveViewState() ?? undefined; + const input2State = this.input2View.editor.saveViewState() ?? undefined; + const focusIndex = [this.input1View.editor, this.input2View.editor, this.inputResultView.editor].findIndex(editor => editor.hasWidgetFocus()); + return { ...result, input1State, input2State, focusIndex }; + } + + + protected tracksEditorViewState(input: EditorInput): boolean { + return input instanceof MergeEditorInput; + } +} + +type IMergeEditorViewState = ICodeEditorViewState & { + readonly input1State?: ICodeEditorViewState; + readonly input2State?: ICodeEditorViewState; + readonly focusIndex: number; +}; + + +function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: DocumentMapping | undefined, source: MappingDirection) { + if (!mapping) { + return; + } + + const visibleRanges = scrollingEditor.getVisibleRanges(); + if (visibleRanges.length === 0) { + return; + } + const topLineNumber = visibleRanges[0].startLineNumber - 1; + + const result = mapping.getMappingContaining(topLineNumber, source); + const sourceRange = result.getRange(source); + const targetRange = result.getRange(getOppositeDirection(source)); + + const resultStartTopPx = targetEditor.getTopForLineNumber(targetRange.startLineNumber); + const resultEndPx = targetEditor.getTopForLineNumber(targetRange.endLineNumberExclusive); + + const sourceStartTopPx = scrollingEditor.getTopForLineNumber(sourceRange.startLineNumber); + const sourceEndPx = scrollingEditor.getTopForLineNumber(sourceRange.endLineNumberExclusive); + + const factor = Math.min((scrollingEditor.getScrollTop() - sourceStartTopPx) / (sourceEndPx - sourceStartTopPx), 1); + const resultScrollPosition = resultStartTopPx + (resultEndPx - resultStartTopPx) * factor; + + targetEditor.setScrollTop(resultScrollPosition, ScrollType.Immediate); +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts new file mode 100644 index 00000000000..b53549fea4b --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { findLast, lastOrDefault } from 'vs/base/common/arrays'; +import { ScrollType } from 'vs/editor/common/editorCommon'; +import { derivedObservable, derivedObservableWithWritableCache, IReader, ITransaction, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; +import { ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; +import { elementAtOrUndefined } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { CodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView'; +import { InputCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView'; +import { ResultCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView'; + +export class MergeEditorViewModel { + private readonly lastFocusedEditor = derivedObservableWithWritableCache< + CodeEditorView | undefined + >('lastFocusedEditor', (reader, lastValue) => { + const editors = [ + this.inputCodeEditorView1, + this.inputCodeEditorView2, + this.resultCodeEditorView, + ]; + return editors.find((e) => e.isFocused.read(reader)) || lastValue; + }); + + private readonly manuallySetActiveModifiedBaseRange = new ObservableValue< + ModifiedBaseRange | undefined + >(undefined, 'manuallySetActiveModifiedBaseRange'); + + private getRange(editor: CodeEditorView, modifiedBaseRange: ModifiedBaseRange, reader: IReader | undefined): LineRange { + if (editor === this.resultCodeEditorView) { + return this.model.getRangeInResult(modifiedBaseRange.baseRange, reader); + } else { + const input = editor === this.inputCodeEditorView1 ? 1 : 2; + return modifiedBaseRange.getInputRange(input); + } + } + + public readonly activeModifiedBaseRange = derivedObservable( + 'activeModifiedBaseRange', + (reader) => { + const focusedEditor = this.lastFocusedEditor.read(reader); + if (!focusedEditor) { + return this.manuallySetActiveModifiedBaseRange.read(reader); + } + const cursorLineNumber = focusedEditor.cursorLineNumber.read(reader); + if (!cursorLineNumber) { + return undefined; + } + + const modifiedBaseRanges = this.model.modifiedBaseRanges.read(reader); + return modifiedBaseRanges.find((r) => { + const range = this.getRange(focusedEditor, r, reader); + return range.isEmpty + ? range.startLineNumber === cursorLineNumber + : range.contains(cursorLineNumber); + }); + } + ); + + constructor( + public readonly model: MergeEditorModel, + private readonly inputCodeEditorView1: InputCodeEditorView, + private readonly inputCodeEditorView2: InputCodeEditorView, + private readonly resultCodeEditorView: ResultCodeEditorView + ) { } + + public setState( + baseRange: ModifiedBaseRange, + state: ModifiedBaseRangeState, + tx: ITransaction + ): void { + this.manuallySetActiveModifiedBaseRange.set(baseRange, tx); + this.lastFocusedEditor.clearCache(tx); + this.model.setState(baseRange, state, true, tx); + } + + public goToConflict(getModifiedBaseRange: (editor: CodeEditorView, curLineNumber: number) => ModifiedBaseRange | undefined): void { + const lastFocusedEditor = this.lastFocusedEditor.get(); + if (!lastFocusedEditor) { + return; + } + const curLineNumber = lastFocusedEditor.editor.getPosition()?.lineNumber; + if (curLineNumber === undefined) { + return; + } + const modifiedBaseRange = getModifiedBaseRange(lastFocusedEditor, curLineNumber); + if (modifiedBaseRange) { + const range = this.getRange(lastFocusedEditor, modifiedBaseRange, undefined); + lastFocusedEditor.editor.setPosition({ + lineNumber: range.startLineNumber, + column: lastFocusedEditor.editor.getModel()!.getLineFirstNonWhitespaceColumn(range.startLineNumber), + }); + lastFocusedEditor.editor.revealLinesNearTop(range.startLineNumber, range.endLineNumberExclusive, ScrollType.Smooth); + } + } + + public goToNextConflict(): void { + this.goToConflict( + (e, l) => + this.model.modifiedBaseRanges + .get() + .find((r) => this.getRange(e, r, undefined).startLineNumber > l) || + elementAtOrUndefined(this.model.modifiedBaseRanges.get(), 0) + ); + } + + public goToPreviousConflict(): void { + this.goToConflict( + (e, l) => + findLast( + this.model.modifiedBaseRanges.get(), + (r) => this.getRange(e, r, undefined).endLineNumberExclusive < l + ) || lastOrDefault(this.model.modifiedBaseRanges.get()) + ); + } + + public toggleActiveConflict(inputNumber: 1 | 2): void { + const activeModifiedBaseRange = this.activeModifiedBaseRange.get(); + if (!activeModifiedBaseRange) { + return; + } + transaction(tx => { + this.setState( + activeModifiedBaseRange, + this.model.getState(activeModifiedBaseRange).get().toggle(inputNumber), + tx + ); + }); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts new file mode 100644 index 00000000000..9ec46266ae2 --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export type MergeEditorLayoutTypes = 'mixed' | 'columns'; + +export const ctxIsMergeEditor = new RawContextKey<boolean>('isMergeEditor', false); +export const ctxMergeEditorLayout = new RawContextKey<MergeEditorLayoutTypes>('mergeEditorLayout', 'mixed'); +export const ctxBaseResourceScheme = new RawContextKey<string>('baseResourceScheme', ''); diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts new file mode 100644 index 00000000000..9963829694f --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts @@ -0,0 +1,269 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert = require('assert'); +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; +import { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { transaction } from 'vs/workbench/contrib/audioCues/browser/observable'; +import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer'; +import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; + +suite('merge editor model', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('prepend line', async () => { + await testMergeModel( + { + "languageId": "plaintext", + "base": "line1\nline2", + "input1": "0\nline1\nline2", + "input2": "0\nline1\nline2", + "result": "" + }, + model => { + assert.deepStrictEqual(model.getProjections(), { + base: '⟦⟧₀line1\nline2', + input1: '⟦0\n⟧₀line1\nline2', + input2: '⟦0\n⟧₀line1\nline2', + }); + + model.toggleConflict(0, 1); + assert.deepStrictEqual( + { result: model.getResult() }, + { result: '0\nline1\nline2' } + ); + + model.toggleConflict(0, 2); + assert.deepStrictEqual( + { result: model.getResult() }, + ({ result: "0\n0\nline1\nline2" }) + ); + } + ); + }); + + test('empty base', async () => { + await testMergeModel( + { + "languageId": "plaintext", + "base": "", + "input1": "input1", + "input2": "input2", + "result": "" + }, + model => { + assert.deepStrictEqual(model.getProjections(), ({ base: "⟦⟧₀", input1: "⟦input1⟧₀", input2: "⟦input2⟧₀" })); + + model.toggleConflict(0, 1); + assert.deepStrictEqual( + { result: model.getResult() }, + ({ result: "input1" }) + ); + + model.toggleConflict(0, 2); + assert.deepStrictEqual( + { result: model.getResult() }, + ({ result: "input2" }) + ); + } + ); + }); + + test('can merge word changes', async () => { + await testMergeModel( + { + "languageId": "plaintext", + "base": "hello", + "input1": "hallo", + "input2": "helloworld", + "result": "" + }, + model => { + assert.deepStrictEqual(model.getProjections(), { + base: '⟦hello⟧₀', + input1: '⟦hallo⟧₀', + input2: '⟦helloworld⟧₀', + }); + + model.toggleConflict(0, 1); + model.toggleConflict(0, 2); + + assert.deepStrictEqual( + { result: model.getResult() }, + { result: 'halloworld' } + ); + } + ); + + }); + + test('can combine insertions at end of document', async () => { + await testMergeModel( + { + "languageId": "plaintext", + "base": "Zürich\nBern\nBasel\nChur\nGenf\nThun", + "input1": "Zürich\nBern\nChur\nDavos\nGenf\nThun\nfunction f(b:boolean) {}", + "input2": "Zürich\nBern\nBasel (FCB)\nChur\nGenf\nThun\nfunction f(a:number) {}", + "result": "Zürich\nBern\nBasel\nChur\nDavos\nGenf\nThun" + }, + model => { + assert.deepStrictEqual(model.getProjections(), { + base: 'Zürich\nBern\n⟦Basel\n⟧₀Chur\n⟦⟧₁Genf\nThun⟦⟧₂', + input1: + 'Zürich\nBern\n⟦⟧₀Chur\n⟦Davos\n⟧₁Genf\nThun\n⟦function f(b:boolean) {}⟧₂', + input2: + 'Zürich\nBern\n⟦Basel (FCB)\n⟧₀Chur\n⟦⟧₁Genf\nThun\n⟦function f(a:number) {}⟧₂', + }); + + model.toggleConflict(2, 1); + model.toggleConflict(2, 2); + + assert.deepStrictEqual( + { result: model.getResult() }, + { + result: + 'Zürich\nBern\nBasel\nChur\nDavos\nGenf\nThun\nfunction f(b:boolean) {}\nfunction f(a:number) {}', + } + ); + } + ); + }); +}); + +async function testMergeModel( + options: MergeModelOptions, + fn: (model: MergeModelInterface) => void +): Promise<void> { + const disposables = new DisposableStore(); + const modelInterface = disposables.add( + new MergeModelInterface(options, createModelServices(disposables)) + ); + await modelInterface.mergeModel.onInitialized; + await fn(modelInterface); + disposables.dispose(); +} + +interface MergeModelOptions { + languageId: string; + input1: string; + input2: string; + base: string; + result: string; +} + +function toSmallNumbersDec(value: number): string { + const smallNumbers = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉']; + return value.toString().split('').map(c => smallNumbers[parseInt(c)]).join(''); +} + +class MergeModelInterface extends Disposable { + public readonly mergeModel: MergeEditorModel; + + constructor(options: MergeModelOptions, instantiationService: IInstantiationService) { + super(); + const input1TextModel = this._register(createTextModel(options.input1, options.languageId)); + const input2TextModel = this._register(createTextModel(options.input2, options.languageId)); + const baseTextModel = this._register(createTextModel(options.base, options.languageId)); + const resultTextModel = this._register(createTextModel(options.result, options.languageId)); + this.mergeModel = this._register(instantiationService.createInstance(MergeEditorModel, + baseTextModel, + input1TextModel, + '', + '', + '', + input2TextModel, + '', + '', + '', + resultTextModel, + { + async computeDiff(textModel1, textModel2) { + const result = EditorSimpleWorker.computeDiff(textModel1, textModel2, false, 10000); + if (!result) { + return { diffs: null }; + } + return { + diffs: EditorWorkerServiceDiffComputer.fromDiffComputationResult( + result, + textModel1, + textModel2 + ), + }; + }, + } + )); + } + + getProjections(): unknown { + interface LabeledRange { + range: Range; + label: string; + } + function applyRanges(textModel: ITextModel, ranges: LabeledRange[]): void { + textModel.applyEdits(ranges.map(({ range, label }) => ({ + range: range, + text: `⟦${textModel.getValueInRange(range)}⟧${label}`, + }))); + } + const baseRanges = this.mergeModel.modifiedBaseRanges.get(); + + const baseTextModel = createTextModel(this.mergeModel.base.getValue()); + applyRanges( + baseTextModel, + baseRanges.map<LabeledRange>((r, idx) => ({ + range: r.baseRange.toRange(), + label: toSmallNumbersDec(idx), + })) + ); + + const input1TextModel = createTextModel(this.mergeModel.input1.getValue()); + applyRanges( + input1TextModel, + baseRanges.map<LabeledRange>((r, idx) => ({ + range: r.input1Range.toRange(), + label: toSmallNumbersDec(idx), + })) + ); + + const input2TextModel = createTextModel(this.mergeModel.input2.getValue()); + applyRanges( + input2TextModel, + baseRanges.map<LabeledRange>((r, idx) => ({ + range: r.input2Range.toRange(), + label: toSmallNumbersDec(idx), + })) + ); + + const result = { + base: baseTextModel.getValue(), + input1: input1TextModel.getValue(), + input2: input2TextModel.getValue(), + }; + baseTextModel.dispose(); + input1TextModel.dispose(); + input2TextModel.dispose(); + return result; + } + + toggleConflict(conflictIdx: number, inputNumber: 1 | 2): void { + const baseRange = this.mergeModel.modifiedBaseRanges.get()[conflictIdx]; + if (!baseRange) { + throw new Error(); + } + const state = this.mergeModel.getState(baseRange).get(); + transaction(tx => { + this.mergeModel.setState(baseRange, state.toggle(inputNumber), true, tx); + }); + } + + getResult(): string { + return this.mergeModel.result.getValue(); + } +} 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 8fbc0999419..712c2369044 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -285,15 +285,15 @@ export class NotebookClipboardContribution extends Disposable { } if (PasteAction) { - PasteAction.addImplementation(PRIORITY, 'notebook-clipboard', accessor => { + this._register(PasteAction.addImplementation(PRIORITY, 'notebook-clipboard', accessor => { return this.runPasteAction(accessor); - }); + })); } if (CutAction) { - CutAction.addImplementation(PRIORITY, 'notebook-clipboard', accessor => { + this._register(CutAction.addImplementation(PRIORITY, 'notebook-clipboard', accessor => { return this.runCutAction(accessor); - }); + })); } } 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 920669194f6..285dae8f94e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { groupBy } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { compareIgnoreCase, uppercaseFirstLetter } from 'vs/base/common/strings'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import * as nls from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -14,26 +17,27 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; -import { IQuickInputButton, IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { ViewContainerLocation } from 'vs/workbench/common/views'; -import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { CENTER_ACTIVE_CELL } from 'vs/workbench/contrib/notebook/browser/contrib/navigation/arrow'; import { NOTEBOOK_ACTIONS_CATEGORY, SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { getNotebookEditorFromEditorPane, INotebookEditor, KERNEL_EXTENSIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getNotebookEditorFromEditorPane, INotebookEditor, INotebookExtensionRecommendation, KERNEL_RECOMMENDATIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { configureKernelIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernel, INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT, NOTEBOOK_MISSING_KERNEL_EXTENSION } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { INotebookKernel, INotebookKernelService, ISourceAction } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; registerAction2(class extends Action2 { constructor() { @@ -48,7 +52,7 @@ registerAction2(class extends Action2 { id: MenuId.EditorTitle, when: ContextKeyExpr.and( NOTEBOOK_IS_ACTIVE_EDITOR, - ContextKeyExpr.or(NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), NOTEBOOK_MISSING_KERNEL_EXTENSION), + ContextKeyExpr.or(NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), NOTEBOOK_KERNEL_SOURCE_COUNT.notEqualsTo(0), NOTEBOOK_MISSING_KERNEL_EXTENSION), ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) ), group: 'navigation', @@ -56,7 +60,7 @@ registerAction2(class extends Action2 { }, { id: MenuId.NotebookToolbar, when: ContextKeyExpr.and( - ContextKeyExpr.or(NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), NOTEBOOK_MISSING_KERNEL_EXTENSION), + ContextKeyExpr.or(NOTEBOOK_KERNEL_COUNT.notEqualsTo(0), NOTEBOOK_KERNEL_SOURCE_COUNT.notEqualsTo(0), NOTEBOOK_MISSING_KERNEL_EXTENSION), ContextKeyExpr.equals('config.notebook.globalToolbar', true) ), group: 'status', @@ -103,10 +107,13 @@ registerAction2(class extends Action2 { ): Promise<boolean> { const notebookKernelService = accessor.get(INotebookKernelService); const editorService = accessor.get(IEditorService); + const productService = accessor.get(IProductService); const quickInputService = accessor.get(IQuickInputService); const labelService = accessor.get(ILabelService); const logService = accessor.get(ILogService); const paneCompositeService = accessor.get(IPaneCompositePartService); + const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const extensionHostService = accessor.get(IExtensionService); let editor: INotebookEditor | undefined; if (context !== undefined && 'notebookEditorId' in context) { @@ -157,100 +164,199 @@ registerAction2(class extends Action2 { } } - if (!newKernel) { - type KernelPick = IQuickPickItem & { kernel: INotebookKernel }; - const configButton: IQuickInputButton = { - iconClass: ThemeIcon.asClassName(configureKernelIcon), - tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default for '{0}' notebooks", editor.textModel.viewType) + if (newKernel) { + notebookKernelService.selectKernelForNotebook(newKernel, notebook); + return true; + } + + type KernelPick = IQuickPickItem & { kernel: INotebookKernel }; + type SourcePick = IQuickPickItem & { action: ISourceAction }; + + function toQuickPick(kernel: INotebookKernel) { + const res = <KernelPick>{ + kernel, + picked: kernel.id === selected?.id, + label: kernel.label, + description: kernel.description, + detail: kernel.detail }; - function toQuickPick(kernel: INotebookKernel) { - const res = <KernelPick>{ - kernel, - picked: kernel.id === selected?.id, - label: kernel.label, - description: kernel.description, - detail: kernel.detail, - buttons: [configButton] - }; - if (kernel.id === selected?.id) { - if (!res.description) { - res.description = nls.localize('current1', "Currently Selected"); - } else { - res.description = nls.localize('current2', "{0} - Currently Selected", res.description); - } + if (kernel.id === selected?.id) { + if (!res.description) { + res.description = nls.localize('current1', "Currently Selected"); + } else { + res.description = nls.localize('current2', "{0} - Currently Selected", res.description); } - return res; } - const quickPickItems: QuickPickInput<IQuickPickItem | KernelPick>[] = []; - if (all.length) { - // Always display suggested kernels on the top. - if (suggestions.length) { - quickPickItems.push({ - type: 'separator', - label: nls.localize('suggestedKernels', "Suggested") - }); - quickPickItems.push(...suggestions.map(toQuickPick)); - } - - // Next display all of the kernels grouped by categories or extensions. - // If we don't have a kind, always display those at the bottom. - const picks = all.filter(item => !suggestions.includes(item)).map(toQuickPick); - const kernelsPerCategory = groupBy(picks, (a, b) => compareIgnoreCase(a.kernel.kind || 'z', b.kernel.kind || 'z')); - kernelsPerCategory.forEach(items => { - quickPickItems.push({ - type: 'separator', - label: items[0].kernel.kind || nls.localize('otherKernelKinds', "Other") - }); - quickPickItems.push(...items); + return res; + } + const quickPickItems: QuickPickInput<IQuickPickItem | KernelPick | SourcePick>[] = []; + if (all.length) { + // Always display suggested kernels on the top. + if (suggestions.length) { + quickPickItems.push({ + type: 'separator', + label: nls.localize('suggestedKernels', "Suggested") }); + quickPickItems.push(...suggestions.map(toQuickPick)); } - if (!all.find(item => item.type === NotebookKernelType.Resolved)) { - // there is no resolved kernel, show the install from marketplace + // Next display all of the kernels grouped by categories or extensions. + // If we don't have a kind, always display those at the bottom. + const picks = all.filter(item => !suggestions.includes(item)).map(toQuickPick); + const kernelsPerCategory = groupBy(picks, (a, b) => compareIgnoreCase(a.kernel.kind || 'z', b.kernel.kind || 'z')); + kernelsPerCategory.forEach(items => { quickPickItems.push({ - id: 'install', - label: nls.localize('installKernels', "Install kernels from the marketplace"), + type: 'separator', + label: items[0].kernel.kind || nls.localize('otherKernelKinds', "Other") }); - } + quickPickItems.push(...items); + }); + } - const pick = await quickInputService.pick(quickPickItems, { - placeHolder: selected - ? nls.localize('prompt.placeholder.change', "Change kernel for '{0}'", labelService.getUriLabel(notebook.uri, { relative: true })) - : nls.localize('prompt.placeholder.select', "Select kernel for '{0}'", labelService.getUriLabel(notebook.uri, { relative: true })), - onDidTriggerItemButton: (context) => { - if ('kernel' in context.item) { - notebookKernelService.selectKernelForNotebookType(context.item.kernel, notebook.viewType); - } - } + const sourceActions = notebookKernelService.getSourceActions(); + if (sourceActions.length) { + quickPickItems.push({ + type: 'separator', + // label: nls.localize('sourceActions', "") }); - if (pick) { - if (pick.id === 'install') { - await this._showKernelExtension(paneCompositeService, notebook.viewType); - } else if ('kernel' in pick) { - newKernel = pick.kernel; - } + sourceActions.forEach(sourceAction => { + const res = <SourcePick>{ + action: sourceAction, + picked: false, + label: sourceAction.action.label, + }; + + quickPickItems.push(res); + }); + } + + let suggestedExtension: INotebookExtensionRecommendation | undefined; + if (!all.length && !sourceActions.length) { + const activeNotebookModel = getNotebookEditorFromEditorPane(editorService.activeEditorPane)?.textModel; + if (activeNotebookModel) { + const language = this.getSuggestedLanguage(activeNotebookModel); + suggestedExtension = language ? this.getSuggestedKernelFromLanguage(activeNotebookModel.viewType, language) : undefined; + } + if (suggestedExtension) { + // We have a suggested kernel, show an option to install it + quickPickItems.push({ + id: 'installSuggested', + description: suggestedExtension.displayName ?? suggestedExtension.extensionId, + label: nls.localize('installSuggestedKernel', '$({0}) Install suggested extensions', Codicon.lightbulb.id), + }); } + // there is no kernel, show the install from marketplace + quickPickItems.push({ + id: 'install', + label: nls.localize('searchForKernels', "Browse marketplace for kernel extensions"), + }); } - if (newKernel) { - notebookKernelService.selectKernelForNotebook(newKernel, notebook); - return true; + const pick = await quickInputService.pick(quickPickItems, { + placeHolder: selected + ? nls.localize('prompt.placeholder.change', "Change kernel for '{0}'", labelService.getUriLabel(notebook.uri, { relative: true })) + : nls.localize('prompt.placeholder.select', "Select kernel for '{0}'", labelService.getUriLabel(notebook.uri, { relative: true })) + }); + + if (pick) { + if ('kernel' in pick) { + newKernel = pick.kernel; + notebookKernelService.selectKernelForNotebook(newKernel, notebook); + return true; + } + + // actions + + if (pick.id === 'install') { + await this._showKernelExtension( + paneCompositeService, + extensionWorkbenchService, + extensionHostService, + notebook.viewType + ); + // suggestedExtension must be defined for this option to be shown, but still check to make TS happy + } else if (pick.id === 'installSuggested' && suggestedExtension) { + await this._showKernelExtension( + paneCompositeService, + extensionWorkbenchService, + extensionHostService, + notebook.viewType, + suggestedExtension.extensionId, + productService.quality !== 'stable' + ); + } else if ('action' in pick) { + // selected explicilty, it should trigger the execution? + pick.action.runAction(); + } } + return false; } - private async _showKernelExtension(paneCompositePartService: IPaneCompositePartService, viewType: string) { - const viewlet = await paneCompositePartService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; + /** + * Examine the most common language in the notebook + * @param notebookTextModel The notebook text model + * @returns What the suggested language is for the notebook. Used for kernal installing + */ + private getSuggestedLanguage(notebookTextModel: NotebookTextModel): string | undefined { + const metaData = notebookTextModel.metadata; + let suggestedKernelLanguage: string | undefined = (metaData.custom as any)?.metadata?.language_info?.name; + // TODO how do we suggest multi language notebooks? + if (!suggestedKernelLanguage) { + const cellLanguages = notebookTextModel.cells.map(cell => cell.language).filter(language => language !== 'markdown'); + // Check if cell languages is all the same + if (cellLanguages.length > 1) { + const firstLanguage = cellLanguages[0]; + if (cellLanguages.every(language => language === firstLanguage)) { + suggestedKernelLanguage = firstLanguage; + } + } + } + return suggestedKernelLanguage; + } - const extId = KERNEL_EXTENSIONS.get(viewType); + /** + * Given a language and notebook view type suggest a kernel for installation + * @param language The language to find a suggested kernel extension for + * @returns A recommednation object for the recommended extension, else undefined + */ + private getSuggestedKernelFromLanguage(viewType: string, language: string): INotebookExtensionRecommendation | undefined { + const recommendation = KERNEL_RECOMMENDATIONS.get(viewType)?.get(language); + return recommendation; + } + + private async _showKernelExtension( + paneCompositePartService: IPaneCompositePartService, + extensionWorkbenchService: IExtensionsWorkbenchService, + extensionService: IExtensionService, + viewType: string, + extId?: string, + isInsiders?: boolean + ) { + // If extension id is provided attempt to install the extension as the user has requested the suggested ones be installed if (extId) { - view?.search(`@id:${extId}`); - } else { - const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join(''); - view?.search(`@tag:notebookKernel${pascalCased}`); + const extension = (await extensionWorkbenchService.getExtensions([{ id: extId }], CancellationToken.None))[0]; + const canInstall = await extensionWorkbenchService.canInstall(extension); + // If we can install then install it, otherwise we will fall out into searching the viewlet + if (canInstall) { + await extensionWorkbenchService.install( + extension, + { + installPreReleaseVersion: isInsiders ?? false, + context: { skipWalkthrough: true } + }, + ProgressLocation.Notification + ); + await extensionService.activateByEvent(`onNotebook:${viewType}`); + return; + } } + + const viewlet = await paneCompositePartService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); + const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; + const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join(''); + view?.search(`@tag:notebookKernel${pascalCased}`); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts index f1ff2386aa7..50fe4315bf0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts @@ -12,7 +12,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; -import { StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/browser/findController'; +import { FindStartFocusAction, getSelectionSearchString, IFindStartOptions, StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/browser/findController'; import { localize } from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -24,6 +24,7 @@ import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/brow import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; registerNotebookContribution(NotebookFindWidget.id, NotebookFindWidget); @@ -91,6 +92,23 @@ function notebookContainsTextModel(uri: URI, textModel: ITextModel) { return false; } +function getSearchString(editor: ICodeEditor, opts: IFindStartOptions) { + // Get the search string result, following the same logic in _start function in 'vs/editor/contrib/find/browser/findController' + let searchString = ''; + if (opts.seedSearchStringFromSelection === 'single') { + const selectionSearchString = getSelectionSearchString(editor, opts.seedSearchStringFromSelection, opts.seedSearchStringFromNonEmptySelection); + if (selectionSearchString) { + searchString = selectionSearchString; + } + } else if (opts.seedSearchStringFromSelection === 'multiple' && !opts.updateSearchScope) { + const selectionSearchString = getSelectionSearchString(editor, opts.seedSearchStringFromSelection); + if (selectionSearchString) { + searchString = selectionSearchString; + } + } + return searchString; +} + StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { const editorService = accessor.get(IEditorService); @@ -112,7 +130,19 @@ StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: } const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - controller.show(); + + const searchString = getSearchString(codeEditor, { + forceRevealReplace: false, + seedSearchStringFromSelection: codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never' ? 'single' : 'none', + seedSearchStringFromNonEmptySelection: codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection', + seedSearchStringFromGlobalClipboard: codeEditor.getOption(EditorOption.find).globalFindClipboard, + shouldFocus: FindStartFocusAction.FocusFindInput, + shouldAnimate: true, + updateSearchScope: false, + loop: codeEditor.getOption(EditorOption.find).loop + }); + + controller.show(searchString); return true; }); @@ -125,8 +155,20 @@ StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeE } const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + + const searchString = getSearchString(codeEditor, { + forceRevealReplace: false, + seedSearchStringFromSelection: codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never' ? 'single' : 'none', + seedSearchStringFromNonEmptySelection: codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection', + seedSearchStringFromGlobalClipboard: codeEditor.getOption(EditorOption.find).globalFindClipboard, + shouldFocus: FindStartFocusAction.FocusFindInput, + shouldAnimate: true, + updateSearchScope: false, + loop: codeEditor.getOption(EditorOption.find).loop + }); + if (controller) { - controller.replace(); + controller.replace(searchString); return true; } 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 6b7a050bfea..59f060f6421 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -290,7 +290,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e))); this._scopedContextKeyService = contextKeyService.createScoped(this._domNode); - let progressContainer = dom.$('.find-replace-progress'); + const progressContainer = dom.$('.find-replace-progress'); this._progressBar = new ProgressBar(progressContainer); this._register(attachProgressBarStyler(this._progressBar, this._themeService)); this._domNode.appendChild(progressContainer); @@ -564,7 +564,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { private _updateButtons(): void { this._findInput.setEnabled(this._isVisible); this._replaceInput.setEnabled(this._isVisible && this._isReplaceVisible); - let findInputIsNonEmpty = (this._state.searchString.length > 0); + const findInputIsNonEmpty = (this._state.searchString.length > 0); this._replaceBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty); this._replaceAllBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty); @@ -615,7 +615,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { } public show(initialInput?: string, options?: { focus?: boolean }): void { - if (initialInput && !this._isVisible) { + if (initialInput) { this._findInput.setValue(initialInput); } @@ -632,11 +632,11 @@ export abstract class SimpleFindReplaceWidget extends Widget { } public showWithReplace(initialInput?: string, replaceInput?: string): void { - if (initialInput && !this._isVisible) { + if (initialInput) { this._findInput.setValue(initialInput); } - if (replaceInput && !this._isVisible) { + if (replaceInput) { this._replaceInput.setValue(replaceInput); } 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 6d2e6f4785e..47d0e6be557 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -80,7 +80,7 @@ registerAction2(class extends Action2 { const edits: ResourceTextEdit[] = []; if (formatEdits) { - for (let edit of formatEdits) { + for (const edit of formatEdits) { edits.push(new ResourceTextEdit(model.uri, edit, model.getVersionId())); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts b/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts index 0a930250070..3e1c47af18a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted.ts @@ -40,7 +40,7 @@ export class NotebookGettingStarted extends Disposable implements IWorkbenchCont const hasOpenedNotebook = HAS_OPENED_NOTEBOOK.bindTo(_contextKeyService); const memento = new Memento('notebookGettingStarted2', _storageService); - const storedValue = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + const storedValue = memento.getMemento(StorageScope.PROFILE, StorageTarget.USER); if (storedValue[hasOpenedNotebookKey]) { hasOpenedNotebook.set(true); } @@ -91,7 +91,7 @@ registerAction2(class NotebookClearNotebookLayoutAction extends Action2 { const storageService = accessor.get(IStorageService); const memento = new Memento('notebookGettingStarted', storageService); - const storedValue = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + const storedValue = memento.getMemento(StorageScope.PROFILE, StorageTarget.USER); storedValue[hasOpenedNotebookKey] = undefined; memento.saveMemento(); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index b0dd1d172c8..8c8a8fb1f59 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; @@ -14,10 +15,9 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; const NOTEBOOK_FOCUS_TOP = 'notebook.focusTop'; const NOTEBOOK_FOCUS_BOTTOM = 'notebook.focusBottom'; @@ -41,14 +41,28 @@ registerAction2(class FocusNextCellAction extends NotebookCellAction { { when: ContextKeyExpr.and( NOTEBOOK_EDITOR_FOCUSED, - ContextKeyExpr.has(InputFocusedContextKey), - EditorContextKeys.editorTextFocus, - NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('top'), - NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none'), - ContextKeyExpr.equals('config.notebook.navigation.allowNavigateToSurroundingCells', true) + ContextKeyExpr.equals('config.notebook.navigation.allowNavigateToSurroundingCells', true), + ContextKeyExpr.and( + ContextKeyExpr.has(InputFocusedContextKey), + EditorContextKeys.editorTextFocus, + NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('top'), + NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none'), + ), ), primary: KeyCode.DownArrow, - weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, // code cell keybinding, focus inside editor: lower weight to not override suggest widget + }, + { + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_FOCUSED, + ContextKeyExpr.equals('config.notebook.navigation.allowNavigateToSurroundingCells', true), + ContextKeyExpr.and( + NOTEBOOK_CELL_TYPE.isEqualTo('markup'), + NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.isEqualTo(false), + NOTEBOOK_CURSOR_NAVIGATION_MODE) + ), + primary: KeyCode.DownArrow, + weight: KeybindingWeight.WorkbenchContrib, // markdown keybinding, focus on list: higher weight to override list.focusDown }, { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED), @@ -77,7 +91,6 @@ registerAction2(class FocusNextCellAction extends NotebookCellAction { const newCell = editor.cellAt(idx + 1); const newFocusMode = newCell.cellKind === CellKind.Markup && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; await editor.focusNotebookCell(newCell, newFocusMode, { focusEditorLine: 1 }); - editor.cursorNavigationMode = true; } }); @@ -87,18 +100,35 @@ registerAction2(class FocusPreviousCellAction extends NotebookCellAction { super({ id: NOTEBOOK_FOCUS_PREVIOUS_EDITOR, title: localize('cursorMoveUp', 'Focus Previous Cell Editor'), - keybinding: { - when: ContextKeyExpr.and( - NOTEBOOK_EDITOR_FOCUSED, - ContextKeyExpr.has(InputFocusedContextKey), - EditorContextKeys.editorTextFocus, - NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('bottom'), - NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none'), - ContextKeyExpr.equals('config.notebook.navigation.allowNavigateToSurroundingCells', true) - ), - primary: KeyCode.UpArrow, - weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - }, + keybinding: [ + { + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_FOCUSED, + ContextKeyExpr.equals('config.notebook.navigation.allowNavigateToSurroundingCells', true), + ContextKeyExpr.and( + ContextKeyExpr.has(InputFocusedContextKey), + EditorContextKeys.editorTextFocus, + NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('bottom'), + NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none'), + ), + ), + primary: KeyCode.UpArrow, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, // code cell keybinding, focus inside editor: lower weight to not override suggest widget + }, + { + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_FOCUSED, + ContextKeyExpr.equals('config.notebook.navigation.allowNavigateToSurroundingCells', true), + ContextKeyExpr.and( + NOTEBOOK_CELL_TYPE.isEqualTo('markup'), + NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.isEqualTo(false), + NOTEBOOK_CURSOR_NAVIGATION_MODE + ) + ), + primary: KeyCode.UpArrow, + weight: KeybindingWeight.WorkbenchContrib, // markdown keybinding, focus on list: higher weight to override list.focusDown + } + ], }); } @@ -119,7 +149,6 @@ registerAction2(class FocusPreviousCellAction extends NotebookCellAction { const newCell = editor.cellAt(idx - 1); const newFocusMode = newCell.cellKind === CellKind.Markup && newCell.getEditState() === CellEditState.Preview ? 'container' : 'editor'; await editor.focusNotebookCell(newCell, newFocusMode, { focusEditorLine: newCell.textBuffer.getLineCount() }); - editor.cursorNavigationMode = true; } }); 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 473ff42494f..da58d351ae4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -8,7 +8,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, IDisposable, Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IActiveNotebookEditor, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealType, IActiveNotebookEditor, ICellViewModel, INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; @@ -583,7 +583,11 @@ export class NotebookCellOutline extends Disposable implements IOutline<OutlineE async reveal(entry: OutlineEntry, options: IEditorOptions, sideBySide: boolean): Promise<void> { await this._editorService.openEditor({ resource: entry.cell.uri, - options: { ...options, override: this._editor.input?.editorId }, + options: { + ...options, + override: this._editor.input?.editorId, + cellRevealType: CellRevealType.NearTopIfOutsideViewport + } as INotebookEditorOptions, }, sideBySide ? SIDE_GROUP : undefined); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index 29d4e867f65..b3af4143741 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -7,9 +7,10 @@ import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/commo import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { getNotebookEditorFromEditorPane, ICellViewModel, ICommonCellViewModelLayoutChangeInfo, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getNotebookEditorFromEditorPane, ICellViewModel, ICommonCellViewModelLayoutChangeInfo, INotebookDeltaCellStatusBarItems, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { CellStatusbarAlignment, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -18,31 +19,37 @@ export class TroubleshootController extends Disposable implements INotebookEdito private readonly _localStore = this._register(new DisposableStore()); private _cellStateListeners: IDisposable[] = []; - private _logging: boolean = false; + private _enabled: boolean = false; + private _cellStatusItems: string[] = []; constructor(private readonly _notebookEditor: INotebookEditor) { super(); this._register(this._notebookEditor.onDidChangeModel(() => { - this._localStore.clear(); - this._cellStateListeners.forEach(listener => listener.dispose()); - - if (!this._notebookEditor.hasModel()) { - return; - } - - this._updateListener(); + this._update(); })); - this._updateListener(); + this._update(); } - toggleLogging(): void { - this._logging = !this._logging; + toggle(): void { + this._enabled = !this._enabled; + this._update(); + } + + private _update() { + this._localStore.clear(); + this._cellStateListeners.forEach(listener => listener.dispose()); + + if (!this._notebookEditor.hasModel()) { + return; + } + + this._updateListener(); } private _log(cell: ICellViewModel, e: any) { - if (this._logging) { + if (this._enabled) { const oldHeight = (this._notebookEditor as NotebookEditorWidget).getViewHeight(cell); console.log(`cell#${cell.handle}`, e, `${oldHeight} -> ${cell.layoutInfo.totalHeight}`); } @@ -73,6 +80,33 @@ export class TroubleshootController extends Disposable implements INotebookEdito dispose(deletedCells); }); })); + + const vm = this._notebookEditor._getViewModel(); + let items: INotebookDeltaCellStatusBarItems[] = []; + + if (this._enabled) { + items = this._getItemsForCells(); + } + + this._cellStatusItems = vm.deltaCellStatusBarItems(this._cellStatusItems, items); + } + + private _getItemsForCells(): INotebookDeltaCellStatusBarItems[] { + const items: INotebookDeltaCellStatusBarItems[] = []; + for (let i = 0; i < this._notebookEditor.getLength(); i++) { + items.push({ + handle: i, + items: [ + <INotebookCellStatusBarItem>{ + text: `index: ${i}`, + alignment: CellStatusbarAlignment.Left, + priority: Number.MAX_SAFE_INTEGER + } + ] + }); + } + + return items; } override dispose() { @@ -102,7 +136,7 @@ registerAction2(class extends Action2 { } const controller = editor.getContribution<TroubleshootController>(TroubleshootController.id); - controller?.toggleLogging(); + controller?.toggle(); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index 68d9d0f8dcc..d0c6379a54b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts @@ -7,7 +7,7 @@ import * as glob from 'vs/base/common/glob'; import { URI, UriComponents } from 'vs/base/common/uri'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { isDocumentExcludePattern, TransientCellMetadata, TransientDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor): { @@ -66,13 +66,13 @@ CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, arg const uri = URI.revive(args.uri as UriComponents); const kernels = notebookKernelService.getMatchingKernel({ uri, viewType: args.viewType }); - return kernels.all.filter(kernel => kernel.type === NotebookKernelType.Resolved).map((provider) => ({ + return kernels.all.map(provider => ({ id: provider.id, label: provider.label, kind: provider.kind, description: provider.description, detail: provider.detail, isPreferred: false, // todo@jrieken,@rebornix - preloads: (provider as IResolvedNotebookKernel).preloadUris, + preloads: provider.preloadUris, })); }); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 1c601fbf36f..f7b01782aa0 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -10,7 +10,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, cellRangeToViewCells } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { ICellRange, isICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorCommandsContext } from 'vs/workbench/common/editor'; @@ -276,7 +276,7 @@ export abstract class NotebookCellAction<T = INotebookCellActionContext> extends abstract override runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void>; } -export const executeNotebookCondition = ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0); +export const executeNotebookCondition = ContextKeyExpr.or(ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), ContextKeyExpr.greater(NOTEBOOK_KERNEL_SOURCE_COUNT.key, 0)); interface IMultiCellArgs { ranges: ICellRange[]; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index 351b57c8bef..3e54598b92b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -15,7 +15,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { EditorsOrder } from 'vs/workbench/common/editor'; import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { cellExecutionArgs, CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, executeNotebookCondition, getContextFromActiveEditor, getContextFromUri, INotebookActionContext, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NotebookAction, NotebookCellAction, NotebookMultiCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, parseMultiCellExecutionArgs } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_MISSING_KERNEL_EXTENSION } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT, NOTEBOOK_MISSING_KERNEL_EXTENSION } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -41,6 +41,7 @@ export const executeCondition = ContextKeyExpr.and( NOTEBOOK_CELL_TYPE.isEqualTo('code'), ContextKeyExpr.or( ContextKeyExpr.greater(NOTEBOOK_KERNEL_COUNT.key, 0), + ContextKeyExpr.greater(NOTEBOOK_KERNEL_SOURCE_COUNT.key, 0), NOTEBOOK_MISSING_KERNEL_EXTENSION )); @@ -543,6 +544,8 @@ registerAction2(class RevealRunningCellAction extends NotebookAction { super({ id: REVEAL_RUNNING_CELL, title: localize('revealRunningCell', "Go To Running Cell"), + tooltip: localize('revealRunningCell', "Go To Running Cell"), + shortTitle: localize('revealRunningCellShort', "Go To"), precondition: NOTEBOOK_HAS_RUNNING_CELL, menu: [ { @@ -586,7 +589,7 @@ registerAction2(class RevealRunningCellAction extends NotebookAction { if (executingCells[0]) { const cell = context.notebookEditor.getCellByHandle(executingCells[0].cellHandle); if (cell) { - context.notebookEditor.revealInCenter(cell); + context.notebookEditor.focusNotebookCell(cell, 'container'); } } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 1369de699b9..c0951594b1c 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -202,6 +202,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD didDropMarkupCell(cellId: string) { // throw new Error('Method not implemented.'); } + didResizeOutput(cellId: string): void { + // throw new Error('Method not implemented.'); + } protected createEditor(parent: HTMLElement): void { this._rootElement = DOM.append(parent, DOM.$('.notebook-text-diff-editor')); diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index b48922916a0..651082499a8 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -8,12 +8,12 @@ import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { NotebookEditorPriority, NotebookRendererEntrypoint, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -namespace NotebookEditorContribution { - export const type = 'type'; - export const displayName = 'displayName'; - export const selector = 'selector'; - export const priority = 'priority'; -} +const NotebookEditorContribution = Object.freeze({ + type: 'type', + displayName: 'displayName', + selector: 'selector', + priority: 'priority', +}); export interface INotebookEditorContribution { readonly [NotebookEditorContribution.type]: string; @@ -22,16 +22,15 @@ export interface INotebookEditorContribution { readonly [NotebookEditorContribution.priority]?: string; } -namespace NotebookRendererContribution { - - export const id = 'id'; - export const displayName = 'displayName'; - export const mimeTypes = 'mimeTypes'; - export const entrypoint = 'entrypoint'; - export const hardDependencies = 'dependencies'; - export const optionalDependencies = 'optionalDependencies'; - export const requiresMessaging = 'requiresMessaging'; -} +const NotebookRendererContribution = Object.freeze({ + id: 'id', + displayName: 'displayName', + mimeTypes: 'mimeTypes', + entrypoint: 'entrypoint', + hardDependencies: 'dependencies', + optionalDependencies: 'optionalDependencies', + requiresMessaging: 'requiresMessaging', +}); export interface INotebookRendererContribution { readonly [NotebookRendererContribution.id]?: string; @@ -97,85 +96,105 @@ const notebookProviderContribution: IJSONSchema = { } }; +const defaultRendererSnippet = Object.freeze({ id: '', displayName: '', mimeTypes: [''], entrypoint: '' }); + const notebookRendererContribution: IJSONSchema = { description: nls.localize('contributes.notebook.renderer', 'Contributes notebook output renderer provider.'), type: 'array', - defaultSnippets: [{ body: [{ id: '', displayName: '', mimeTypes: [''], entrypoint: '' }] }], + defaultSnippets: [{ body: [defaultRendererSnippet] }], items: { - type: 'object', - required: [ - NotebookRendererContribution.id, - NotebookRendererContribution.displayName, - NotebookRendererContribution.mimeTypes, - NotebookRendererContribution.entrypoint, - ], - properties: { - [NotebookRendererContribution.id]: { - type: 'string', - description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), - }, - [NotebookRendererContribution.displayName]: { - type: 'string', - description: nls.localize('contributes.notebook.renderer.displayName', 'Human readable name of the notebook output renderer.'), - }, - [NotebookRendererContribution.mimeTypes]: { - type: 'array', - description: nls.localize('contributes.notebook.selector', 'Set of globs that the notebook is for.'), - items: { - type: 'string' + defaultSnippets: [{ body: defaultRendererSnippet }], + allOf: [ + { + type: 'object', + required: [ + NotebookRendererContribution.id, + NotebookRendererContribution.displayName, + ], + properties: { + [NotebookRendererContribution.id]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), + }, + [NotebookRendererContribution.displayName]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.displayName', 'Human readable name of the notebook output renderer.'), + }, + [NotebookRendererContribution.hardDependencies]: { + type: 'array', + uniqueItems: true, + items: { type: 'string' }, + markdownDescription: nls.localize('contributes.notebook.renderer.hardDependencies', 'List of kernel dependencies the renderer requires. If any of the dependencies are present in the `NotebookKernel.preloads`, the renderer can be used.'), + }, + [NotebookRendererContribution.optionalDependencies]: { + type: 'array', + uniqueItems: true, + items: { type: 'string' }, + markdownDescription: nls.localize('contributes.notebook.renderer.optionalDependencies', 'List of soft kernel dependencies the renderer can make use of. If any of the dependencies are present in the `NotebookKernel.preloads`, the renderer will be preferred over renderers that don\'t interact with the kernel.'), + }, + [NotebookRendererContribution.requiresMessaging]: { + default: 'never', + enum: [ + 'always', + 'optional', + 'never', + ], + + enumDescriptions: [ + nls.localize('contributes.notebook.renderer.requiresMessaging.always', 'Messaging is required. The renderer will only be used when it\'s part of an extension that can be run in an extension host.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.optional', 'The renderer is better with messaging available, but it\'s not requried.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.never', 'The renderer does not require messaging.'), + ], + description: nls.localize('contributes.notebook.renderer.requiresMessaging', 'Defines how and if the renderer needs to communicate with an extension host, via `createRendererMessaging`. Renderers with stronger messaging requirements may not work in all environments.'), + }, } }, - [NotebookRendererContribution.entrypoint]: { - description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + { oneOf: [ { - type: 'string', + required: [ + NotebookRendererContribution.entrypoint, + NotebookRendererContribution.mimeTypes, + ], + properties: { + [NotebookRendererContribution.mimeTypes]: { + type: 'array', + description: nls.localize('contributes.notebook.selector', 'Set of globs that the notebook is for.'), + items: { + type: 'string' + } + }, + [NotebookRendererContribution.entrypoint]: { + description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + type: 'string', + }, + } }, - // todo@connor4312 + @mjbvz: uncomment this once it's ready for external adoption - // { - // type: 'object', - // required: ['extends', 'path'], - // properties: { - // extends: { - // type: 'string', - // description: nls.localize('contributes.notebook.renderer.entrypoint.extends', 'Existing renderer that this one extends.'), - // }, - // path: { - // type: 'string', - // description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), - // }, - // } - // } + { + required: [ + NotebookRendererContribution.entrypoint, + ], + properties: { + [NotebookRendererContribution.entrypoint]: { + description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + type: 'object', + required: ['extends', 'path'], + properties: { + extends: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.entrypoint.extends', 'Existing renderer that this one extends.'), + }, + path: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + }, + } + }, + } + } ] - }, - [NotebookRendererContribution.hardDependencies]: { - type: 'array', - uniqueItems: true, - items: { type: 'string' }, - markdownDescription: nls.localize('contributes.notebook.renderer.hardDependencies', 'List of kernel dependencies the renderer requires. If any of the dependencies are present in the `NotebookKernel.preloads`, the renderer can be used.'), - }, - [NotebookRendererContribution.optionalDependencies]: { - type: 'array', - uniqueItems: true, - items: { type: 'string' }, - markdownDescription: nls.localize('contributes.notebook.renderer.optionalDependencies', 'List of soft kernel dependencies the renderer can make use of. If any of the dependencies are present in the `NotebookKernel.preloads`, the renderer will be preferred over renderers that don\'t interact with the kernel.'), - }, - [NotebookRendererContribution.requiresMessaging]: { - default: 'never', - enum: [ - 'always', - 'optional', - 'never', - ], - - enumDescriptions: [ - nls.localize('contributes.notebook.renderer.requiresMessaging.always', 'Messaging is required. The renderer will only be used when it\'s part of an extension that can be run in an extension host.'), - nls.localize('contributes.notebook.renderer.requiresMessaging.optional', 'The renderer is better with messaging available, but it\'s not requried.'), - nls.localize('contributes.notebook.renderer.requiresMessaging.never', 'The renderer does not require messaging.'), - ], - description: nls.localize('contributes.notebook.renderer.requiresMessaging', 'Defines how and if the renderer needs to communicate with an extension host, via `createRendererMessaging`. Renderers with stronger messaging requirements may not work in all environments.'), - }, - } + } + ] } }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 75452af0bde..84614d859fe 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -49,13 +49,31 @@ export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; export const KERNEL_EXTENSIONS = new Map<string, string>([ [IPYNB_VIEW_TYPE, JUPYTER_EXTENSION_ID], ]); +// @TODO lramos15, place this in a similar spot to our normal recommendations. +export const KERNEL_RECOMMENDATIONS = new Map<string, Map<string, INotebookExtensionRecommendation>>(); +KERNEL_RECOMMENDATIONS.set(IPYNB_VIEW_TYPE, new Map<string, INotebookExtensionRecommendation>()); +KERNEL_RECOMMENDATIONS.get(IPYNB_VIEW_TYPE)?.set('python', { + extensionId: 'ms-python.python', + displayName: 'Python + Jupyter', +}); + +export interface INotebookExtensionRecommendation { + extensionId: string; + displayName?: string; +} //#endregion //#region Output related types + +// !! IMPORTANT !! ---------------------------------------------------------------------------------- +// NOTE that you MUST update vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts#L1986 +// whenever changing the values of this const enum. The webviewPreloads-files manually inlines these values +// because it cannot have dependencies. +// !! IMPORTANT !! ---------------------------------------------------------------------------------- export const enum RenderOutputType { - Html, - Extension + Html = 0, + Extension = 1 } export interface IRenderPlainHtmlOutput { @@ -277,8 +295,15 @@ export interface INotebookDeltaCellStatusBarItems { items: INotebookCellStatusBarItem[]; } + +export enum CellRevealType { + NearTopIfOutsideViewport, + CenterIfOutsideViewport +} + export interface INotebookEditorOptions extends ITextEditorOptions { readonly cellOptions?: ITextResourceEditorInput; + readonly cellRevealType?: CellRevealType; readonly cellSelections?: ICellRange[]; readonly isReadOnly?: boolean; readonly viewState?: INotebookEditorViewState; @@ -324,6 +349,7 @@ export interface INotebookEditorViewState { focus?: number; editorFocused?: boolean; contributionsState?: { [id: string]: unknown }; + selectedKernelId?: string; } export interface ICellModelDecorations { @@ -407,8 +433,6 @@ export interface INotebookEditor { setFocus(focus: ICellRange): void; getId(): string; - cursorNavigationMode: boolean; - _getViewModel(): INotebookViewModel | undefined; hasModel(): this is IActiveNotebookEditor; dispose(): void; @@ -587,16 +611,6 @@ export interface INotebookEditor { changeModelDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null; /** - * Set decoration key on cells in the range - */ - setEditorDecorations(key: string, range: ICellRange): void; - - /** - * Remove decoration key from the notebook editor - */ - removeEditorDecorations(key: string): void; - - /** * Get a contribution of this editor. * @id Unique identifier of the contribution. * @return The contribution or null if contribution not found. diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index c6b4e71bb47..367c1f80e41 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -97,9 +97,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti } private _updateReadonly(input: NotebookEditorInput): void { - if (this._widget.value) { - this._widget.value.setOptions({ isReadOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); - } + this._widget.value?.setOptions({ isReadOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } get textModel(): NotebookTextModel | undefined { @@ -245,6 +243,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti mark(input.resource, 'editorLoaded'); type WorkbenchNotebookOpenClassification = { + owner: 'rebornix'; scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorService.ts index 77353f6a6a9..15d28dc3e2f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorService.ts @@ -8,7 +8,6 @@ import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/com import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookEditor, INotebookEditorCreationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Event } from 'vs/base/common/event'; -import { INotebookDecorationRenderOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export const INotebookEditorService = createDecorator<INotebookEditorService>('INotebookEditorWidgetService'); @@ -27,8 +26,4 @@ export interface INotebookEditorService { removeNotebookEditor(editor: INotebookEditor): void; getNotebookEditor(editorId: string): INotebookEditor | undefined; listNotebookEditors(): readonly INotebookEditor[]; - - registerEditorDecorationType(key: string, options: INotebookDecorationRenderOptions): void; - removeEditorDecorationType(key: string): void; - resolveEditorDecorationOptions(key: string): INotebookDecorationRenderOptions | undefined; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts index da77b9fc7e9..b8e05ea5a02 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorServiceImpl.ts @@ -12,7 +12,6 @@ import { isCompositeNotebookEditorInput, NotebookEditorInput } from 'vs/workbenc import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { INotebookEditor, INotebookEditorCreationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Emitter } from 'vs/base/common/event'; -import { INotebookDecorationRenderOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { GroupIdentifier } from 'vs/workbench/common/editor'; export class NotebookEditorWidgetService implements INotebookEditorService { @@ -23,7 +22,6 @@ export class NotebookEditorWidgetService implements INotebookEditorService { private readonly _disposables = new DisposableStore(); private readonly _notebookEditors = new Map<string, INotebookEditor>(); - private readonly _decorationOptionProviders = new Map<string, INotebookDecorationRenderOptions>(); private readonly _onNotebookEditorAdd = new Emitter<INotebookEditor>(); private readonly _onNotebookEditorsRemove = new Emitter<INotebookEditor>(); @@ -185,21 +183,4 @@ export class NotebookEditorWidgetService implements INotebookEditorService { listNotebookEditors(): readonly INotebookEditor[] { return [...this._notebookEditors].map(e => e[1]); } - - // --- editor decorations - - registerEditorDecorationType(key: string, options: INotebookDecorationRenderOptions): void { - if (!this._decorationOptionProviders.has(key)) { - this._decorationOptionProviders.set(key, options); - } - } - - removeEditorDecorationType(key: string): void { - this._decorationOptionProviders.delete(key); - this.listNotebookEditors().forEach(editor => editor.removeEditorDecorations(key)); - } - - resolveEditorDecorationOptions(key: string): INotebookDecorationRenderOptions | undefined { - return this._decorationOptionProviders.get(key); - } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 8567bdded40..6047d713272 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -46,10 +46,10 @@ import { registerZIndex, ZIndex } from 'vs/platform/layout/browser/zIndexRegistr import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeground, focusBorder, foreground, iconForeground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, toolbarHoverBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { PANEL_BORDER, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { EDITOR_PANE_BACKGROUND, 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, 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 { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealType, 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'; @@ -66,14 +66,13 @@ import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/v import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; -import { NotebookDecorationCSSRules, NotebookRefCountedStyleSheet } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorDecorations'; import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar'; import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys'; import { NotebookOverviewRuler } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler'; import { ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, INotebookSearchOptions, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; @@ -86,7 +85,6 @@ import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser import { IWebview } from 'vs/workbench/contrib/webview/browser/webview'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; const $ = DOM.$; @@ -239,6 +237,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly onDidReceiveMessage: Event<INotebookWebviewMessage> = this._onDidReceiveMessage.event; private readonly _onDidRenderOutput = this._register(new Emitter<ICellOutputViewModel>()); private readonly onDidRenderOutput = this._onDidRenderOutput.event; + private readonly _onDidResizeOutputEmitter = this._register(new Emitter<ICellViewModel>()); + readonly onDidResizeOutput = this._onDidResizeOutputEmitter.event; //#endregion @@ -271,6 +271,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD private readonly _editorFocus: IContextKey<boolean>; private readonly _outputFocus: IContextKey<boolean>; private readonly _editorEditable: IContextKey<boolean>; + private readonly _cursorNavMode: IContextKey<boolean>; protected readonly _contributions = new Map<string, INotebookEditorContribution>(); private _scrollBeyondLastLine: boolean; private readonly _insetModifyQueueByOutputId = new SequencerByKey<string>(); @@ -312,16 +313,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return this._renderedEditors.get(focused); } - private _cursorNavigationMode: boolean = false; - get cursorNavigationMode(): boolean { - return this._cursorNavigationMode; - } - - set cursorNavigationMode(v: boolean) { - this._cursorNavigationMode = v; - } - - get visibleRanges() { return this._list.visibleRanges || []; } @@ -354,12 +345,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD @ILayoutService private readonly layoutService: ILayoutService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, - @IThemeService private readonly themeService: IThemeService, @ITelemetryService private readonly telemetryService: ITelemetryService, @INotebookExecutionService private readonly notebookExecutionService: INotebookExecutionService, @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, - @IWorkbenchLayoutService private readonly workbenchLayoutService: IWorkbenchLayoutService, ) { super(); this.isEmbedded = creationOptions.isEmbedded ?? false; @@ -443,6 +432,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.scopedContextKeyService); this._outputFocus = NOTEBOOK_OUTPUT_FOCUSED.bindTo(this.scopedContextKeyService); this._editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.scopedContextKeyService); + this._cursorNavMode = NOTEBOOK_CURSOR_NAVIGATION_MODE.bindTo(this.scopedContextKeyService); this._editorEditable.set(!creationOptions.isReadOnly); @@ -929,21 +919,21 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', styleController: (_suffix: string) => { return this._list; }, overrideStyles: { - listBackground: editorBackground, - listActiveSelectionBackground: editorBackground, + listBackground: notebookEditorBackground, + listActiveSelectionBackground: notebookEditorBackground, listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionBackground: notebookEditorBackground, listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, + listFocusBackground: notebookEditorBackground, listFocusForeground: foreground, listHoverForeground: foreground, - listHoverBackground: editorBackground, + listHoverBackground: notebookEditorBackground, listHoverOutline: focusBorder, listFocusOutline: focusBorder, - listInactiveSelectionBackground: editorBackground, + listInactiveSelectionBackground: notebookEditorBackground, listInactiveSelectionForeground: foreground, - listInactiveFocusBackground: editorBackground, - listInactiveFocusOutline: editorBackground, + listInactiveFocusBackground: notebookEditorBackground, + listInactiveFocusOutline: notebookEditorBackground, }, accessibilityProvider: { getAriaLabel: (element) => { @@ -962,10 +952,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return nls.localize('notebookTreeAriaLabel', "Notebook"); } }, - focusNextPreviousDelegate: { - onFocusNext: (applyFocusNext: () => void) => this._updateForCursorNavigationMode(applyFocusNext), - onFocusPrevious: (applyFocusPrevious: () => void) => this._updateForCursorNavigationMode(applyFocusPrevious), - } }, ); this._dndController.setList(this._list); @@ -1013,7 +999,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._register(this._list.onDidChangeFocus(_e => { this._onDidChangeActiveEditor.fire(this); this._onDidChangeActiveCell.fire(); - this._cursorNavigationMode = false; + this._cursorNavMode.set(false); })); this._register(this._list.onContextMenu(e => { @@ -1077,23 +1063,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); } - private async _updateForCursorNavigationMode(applyFocusChange: () => void): Promise<void> { - if (this._cursorNavigationMode) { - // Will fire onDidChangeFocus, resetting the state to Container - applyFocusChange(); - - const newFocusedCell = this._list.getFocusedElements()[0]; - if (newFocusedCell.cellKind === CellKind.Code || newFocusedCell.getEditState() === CellEditState.Editing) { - await this.focusNotebookCell(newFocusedCell, 'editor'); - } else { - // Reset to "Editor", the state has not been consumed - this._cursorNavigationMode = true; - } - } else { - applyFocusChange(); - } - } - getDomNode() { return this._overlayContainer; } @@ -1130,6 +1099,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD }); } type WorkbenchNotebookOpenClassification = { + owner: 'rebornix'; scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; @@ -1150,6 +1120,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.restoreListViewState(viewState); } + this._restoreSelectedKernel(viewState); + // load preloads for matching kernel this._loadKernelPreloads(); @@ -1165,14 +1137,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._backgroundMarkdownRendering(); } - private _isScheduled = false; + private _backgroundMarkdownRenderRunning = false; private _backgroundMarkdownRendering() { - if (this._isScheduled) { + if (this._backgroundMarkdownRenderRunning) { return; } + this._backgroundMarkdownRenderRunning = true; runWhenIdle((deadline) => { - this._isScheduled = false; this._backgroundMarkdownRenderingWithDeadline(deadline); }); } @@ -1181,20 +1153,25 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const endTime = Date.now() + deadline.timeRemaining(); const execute = () => { - if (this._isDisposed) { - return; - } + try { + this._backgroundMarkdownRenderRunning = true; + if (this._isDisposed) { + return; + } - if (!this.viewModel) { - return; - } + if (!this.viewModel) { + return; + } - const firstMarkupCell = this.viewModel.viewCells.find(cell => cell.cellKind === CellKind.Markup && !this._webview?.markupPreviewMapping.has(cell.id)) as MarkupCellViewModel | undefined; - if (!firstMarkupCell) { - return; - } + const firstMarkupCell = this.viewModel.viewCells.find(cell => cell.cellKind === CellKind.Markup && !this._webview?.markupPreviewMapping.has(cell.id) && !this.cellIsHidden(cell)) as MarkupCellViewModel | undefined; + if (!firstMarkupCell) { + return; + } - this.createMarkupPreview(firstMarkupCell); + this.createMarkupPreview(firstMarkupCell); + } finally { + this._backgroundMarkdownRenderRunning = false; + } if (Date.now() < endTime) { setTimeout0(execute); @@ -1241,6 +1218,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const selection = cellOptions.options?.selection; if (selection) { await this.revealLineInCenterIfOutsideViewportAsync(cell, selection.startLineNumber); + } else if (options?.cellRevealType === CellRevealType.NearTopIfOutsideViewport) { + await this.revealNearTopIfOutsideViewportAync(cell); } else { await this.revealInCenterIfOutsideViewportAsync(cell); } @@ -1395,7 +1374,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD didStartDragMarkupCell: that._didStartDragMarkupCell.bind(that), didDragMarkupCell: that._didDragMarkupCell.bind(that), didDropMarkupCell: that._didDropMarkupCell.bind(that), - didEndDragMarkupCell: that._didEndDragMarkupCell.bind(that) + didEndDragMarkupCell: that._didEndDragMarkupCell.bind(that), + didResizeOutput: that._didResizeOutput.bind(that) }, id, resource, { ...this._notebookOptions.computeWebviewOptions(), fontFamily: this._generateFontFamily() @@ -1453,11 +1433,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } hasPendingChangeContentHeight = true; - DOM.scheduleAtNextAnimationFrame(() => { + this._localStore.add(DOM.scheduleAtNextAnimationFrame(() => { hasPendingChangeContentHeight = false; this._updateScrollHeight(); this._onDidChangeContentHeight.fire(this._list.getScrollHeight()); - }, 100); + }, 100)); })); this._localStore.add(this._list.onDidRemoveOutputs(outputs => { @@ -1508,6 +1488,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD dispose(deletedCells); }); + + if (e.splices.some(s => s[2].some(cell => cell.cellKind === CellKind.Markup))) { + this._backgroundMarkdownRendering(); + } })); if (this._dimension) { @@ -1706,6 +1690,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } } + private _restoreSelectedKernel(viewState: INotebookEditorViewState | undefined): void { + if (viewState?.selectedKernelId && this.textModel) { + const kernel = this.notebookKernelService.getMatchingKernel(this.textModel).all.find(k => k.id === viewState.selectedKernelId); + if (kernel) { + this.notebookKernelService.selectKernelForNotebook(kernel, this.textModel); + } + } + } + getEditorViewState(): INotebookEditorViewState { const state = this.viewModel?.getEditorViewState(); if (!state) { @@ -1751,8 +1744,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD contributionsState[id] = contribution.saveViewState(); } } - state.contributionsState = contributionsState; + state.selectedKernelId = this.activeKernel?.id; + return state; } @@ -1836,12 +1830,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._overlayContainer.style.left = `${this._shadowElementViewInfo.left - (elementContainerRect?.left || 0)}px`; this._overlayContainer.style.width = `${dimension ? dimension.width : this._shadowElementViewInfo.width}px`; this._overlayContainer.style.height = `${dimension ? dimension.height : this._shadowElementViewInfo.height}px`; - - const rootContainer = this.workbenchLayoutService.getContainer(Parts.EDITOR_PART); - if (rootContainer) { - const clip = DOM.computeClippingRect(this._overlayContainer, rootContainer); - this._overlayContainer.style.clip = `rect(${clip.top}px, ${clip.right}px, ${clip.bottom}px, ${clip.left}px)`; - } } //#endregion @@ -2024,6 +2012,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._listViewInfoAccessor.revealInCenter(cell); } + revealNearTopIfOutsideViewportAync(cell: ICellViewModel) { + return this._listViewInfoAccessor.revealNearTopIfOutsideViewportAync(cell); + } + async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise<void> { return this._listViewInfoAccessor.revealLineInViewAsync(cell, line); } @@ -2099,61 +2091,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD //#endregion //#region Decorations - private _editorStyleSheets = new Map<string, NotebookRefCountedStyleSheet>(); - private _decorationRules = new Map<string, NotebookDecorationCSSRules>(); - private _decortionKeyToIds = new Map<string, string[]>(); - - private _registerDecorationType(key: string) { - const options = this.notebookEditorService.resolveEditorDecorationOptions(key); - - if (options) { - const styleElement = DOM.createStyleSheet(this._body); - const styleSheet = new NotebookRefCountedStyleSheet({ - removeEditorStyleSheets: (key) => { - this._editorStyleSheets.delete(key); - } - }, key, styleElement); - this._editorStyleSheets.set(key, styleSheet); - this._decorationRules.set(key, new NotebookDecorationCSSRules(this.themeService, styleSheet, { - key, - options, - styleSheet - })); - } - } - - setEditorDecorations(key: string, range: ICellRange): void { - if (!this.viewModel) { - return; - } - - // create css style for the decoration - if (!this._editorStyleSheets.has(key)) { - this._registerDecorationType(key); - } - - const decorationRule = this._decorationRules.get(key); - if (!decorationRule) { - return; - } - - const existingDecorations = this._decortionKeyToIds.get(key) || []; - const newDecorations = this.viewModel.getCellsInRange(range).map(cell => ({ - handle: cell.handle, - options: { className: decorationRule.className, outputClassName: decorationRule.className, topClassName: decorationRule.topClassName } - })); - - this._decortionKeyToIds.set(key, this.deltaCellDecorations(existingDecorations, newDecorations)); - } - - removeEditorDecorations(key: string): void { - if (this._decorationRules.has(key)) { - this._decorationRules.get(key)?.dispose(); - } - - const cellDecorations = this._decortionKeyToIds.get(key); - this.deltaCellDecorations(cellDecorations || [], []); - } deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[] { const ret = this.viewModel?.deltaCellDecorations(oldDecorations, newDecorations) || []; @@ -2232,12 +2169,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._pendingLayouts?.get(cell)!.dispose(); } - let deferred = new DeferredPromise<void>(); + const deferred = new DeferredPromise<void>(); const doLayout = () => { if (this._isDisposed) { return; } + if (!this.viewModel?.hasCell(cell)) { + // Cell removed in the meantime? + return; + } + if (this._list.elementHeight(cell) === height) { return; } @@ -2340,6 +2282,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD cell.focusMode = CellFocusMode.Editor; if (!options?.skipReveal) { if (typeof options?.focusEditorLine === 'number') { + this._cursorNavMode.set(true); await this.revealLineInViewAsync(cell, options.focusEditorLine); const editor = this._renderedEditors.get(cell)!; const focusEditorLine = options.focusEditorLine!; @@ -2391,7 +2334,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.focusElement(cell); this._cellFocusAria(cell, focusItem); if (!options?.skipReveal) { - this.revealInCenterIfOutsideViewport(cell); + if (typeof options?.focusEditorLine === 'number') { + this._cursorNavMode.set(true); + this.revealInView(cell); + } else { + this.revealInCenterIfOutsideViewport(cell); + } } this._list.focusView(); } @@ -2421,7 +2369,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } const outputs = viewCell.outputsViewModels; - for (let output of outputs) { + for (const output of outputs) { const [mimeTypes, pick] = output.resolveMimeTypes(this.textModel!, undefined); if (!mimeTypes.find(mimeType => mimeType.isTrusted) || mimeTypes.length === 0) { continue; @@ -2619,10 +2567,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } - const modelIndex = this.viewModel.getCellIndex(cell); - const foldedRanges = this.viewModel.getHiddenRanges(); - const isVisible = !foldedRanges.some(range => modelIndex >= range.start && modelIndex < range.end); - if (!isVisible) { + if (this.cellIsHidden(cell)) { return; } @@ -2640,6 +2585,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD }); } + private cellIsHidden(cell: ICellViewModel): boolean { + const modelIndex = this.viewModel!.getCellIndex(cell); + const foldedRanges = this.viewModel!.getHiddenRanges(); + return foldedRanges.some(range => modelIndex >= range.start && modelIndex <= range.end); + } + async unhideMarkupPreviews(cells: readonly MarkupCellViewModel[]) { if (!this._webview) { return; @@ -2979,6 +2930,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } } + private _didResizeOutput(cellId: string): void { + const cell = this._getCellById(cellId); + if (cell) { + this._onDidResizeOutputEmitter.fire(cell); + } + } + //#endregion //#region Editor Contributions @@ -3205,6 +3163,13 @@ export const cellEditorBackground = registerColor('notebook.cellEditorBackground hcLight: null }, nls.localize('notebook.cellEditorBackground', "Cell editor background color.")); +export const notebookEditorBackground = registerColor('notebook.editorBackground', { + light: EDITOR_PANE_BACKGROUND, + dark: EDITOR_PANE_BACKGROUND, + hcDark: null, + hcLight: null +}, nls.localize('notebook.editorBackground', "Notebook background color.")); + registerThemingParticipant((theme, collector) => { // add css variable rules @@ -3282,6 +3247,12 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container .action-item { background-color: ${notebookBackground} }`); } + const notebookBackgroundColor = theme.getColor(notebookEditorBackground); + + if (notebookBackgroundColor) { + collector.addRule(`.monaco-workbench .notebookOverlay.notebook-editor { background-color: ${notebookBackgroundColor}; }`); + } + const editorBackgroundColor = theme.getColor(cellEditorBackground) ?? theme.getColor(editorBackground); if (editorBackgroundColor) { collector.addRule(`.notebookOverlay .cell .monaco-editor-background, diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts index 2d4fe4cebe0..de0c18f7638 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts @@ -13,8 +13,8 @@ import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controll import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookTextModel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; -import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; export class NotebookExecutionService implements INotebookExecutionService, IDisposable { declare _serviceBrand: undefined; @@ -38,57 +38,48 @@ export class NotebookExecutionService implements INotebookExecutionService, IDis return; } - let kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); - if (!kernel) { - await this._commandService.executeCommand(SELECT_KERNEL_ID); - kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); + // create cell executions + const cellExecutions: [NotebookCellTextModel, INotebookCellExecution][] = []; + for (const cell of cellsArr) { + const cellExe = this._notebookExecutionStateService.getCellExecution(cell.uri); + if (cell.cellKind !== CellKind.Code || !!cellExe) { + continue; + } + cellExecutions.push([cell, this._notebookExecutionStateService.createCellExecution(notebook.uri, cell.handle)]); } + let kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); if (!kernel) { - return; + kernel = await this.resolveSourceActions(notebook); } - if (kernel.type === NotebookKernelType.Proxy) { - this._activeProxyKernelExecutionToken?.dispose(true); - const tokenSource = this._activeProxyKernelExecutionToken = new CancellationTokenSource(); - const resolved = await kernel.resolveKernel(notebook.uri); - const kernels = this._notebookKernelService.getMatchingKernel(notebook); - const newlyMatchedKernel = kernels.all.find(k => k.id === resolved); - - if (!newlyMatchedKernel) { - return; - } - - kernel = newlyMatchedKernel; - if (tokenSource.token.isCancellationRequested) { - // execution was cancelled but we still need to update the active kernel - this._notebookKernelService.selectKernelForNotebook(kernel, notebook); - return; - } + if (!kernel) { + await this._commandService.executeCommand(SELECT_KERNEL_ID); + kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); } - if (kernel.type === NotebookKernelType.Proxy) { + if (!kernel) { + // clear all pending cell executions + cellExecutions.forEach(cellExe => cellExe[1].complete({})); return; } - const executeCells: NotebookCellTextModel[] = []; - for (const cell of cellsArr) { - const cellExe = this._notebookExecutionStateService.getCellExecution(cell.uri); - if (cell.cellKind !== CellKind.Code || !!cellExe) { - continue; - } + // filter cell executions based on selected kernel + const validCellExecutions: INotebookCellExecution[] = []; + for (const [cell, cellExecution] of cellExecutions) { if (!kernel.supportedLanguages.includes(cell.language)) { - continue; + cellExecution.complete({}); + } else { + validCellExecutions.push(cellExecution); } - executeCells.push(cell); } - if (executeCells.length > 0) { + // request execution + if (validCellExecutions.length > 0) { this._notebookKernelService.selectKernelForNotebook(kernel, notebook); - - const exes = executeCells.map(c => this._notebookExecutionStateService.createCellExecution(kernel!.id, notebook.uri, c.handle)); - await kernel.executeNotebookCellsRequest(notebook.uri, executeCells.map(c => c.handle)); - const unconfirmed = exes.filter(exe => exe.state === NotebookCellExecutionState.Unconfirmed); + await kernel.executeNotebookCellsRequest(notebook.uri, validCellExecutions.map(c => c.cellHandle)); + // the connecting state can change before the kernel resolves executeNotebookCellsRequest + const unconfirmed = validCellExecutions.filter(exe => exe.state === NotebookCellExecutionState.Unconfirmed); if (unconfirmed.length) { this._logService.debug(`NotebookExecutionService#executeNotebookCells completing unconfirmed executions ${JSON.stringify(unconfirmed.map(exe => exe.cellHandle))}`); unconfirmed.forEach(exe => exe.complete({})); @@ -96,16 +87,27 @@ export class NotebookExecutionService implements INotebookExecutionService, IDis } } + private async resolveSourceActions(notebook: INotebookTextModel) { + let kernel: INotebookKernel | undefined; + const info = this._notebookKernelService.getMatchingKernel(notebook); + if (info.all.length === 0) { + // no kernel at all + const sourceActions = this._notebookKernelService.getSourceActions(); + if (sourceActions.length === 1) { + await sourceActions[0].runAction(); + kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); + } + } + + return kernel; + } + async cancelNotebookCellHandles(notebook: INotebookTextModel, cells: Iterable<number>): Promise<void> { const cellsArr = Array.from(cells); this._logService.debug(`NotebookExecutionService#cancelNotebookCellHandles ${JSON.stringify(cellsArr)}`); const kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); if (kernel) { - if (kernel.type === NotebookKernelType.Proxy) { - this._activeProxyKernelExecutionToken?.dispose(true); - } else { - await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); - } + await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts index be0522ab323..e32dbce7ba8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts @@ -14,7 +14,6 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CellEditType, CellUri, ICellEditOperation, NotebookCellExecutionState, NotebookCellInternalMetadata, NotebookTextModelWillAddRemoveEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType, INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecuteUpdate, ICellExecutionComplete, ICellExecutionStateChangedEvent, ICellExecutionStateUpdate, INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; export class NotebookExecutionStateService extends Disposable implements INotebookExecutionStateService { @@ -28,7 +27,6 @@ export class NotebookExecutionStateService extends Disposable implements INotebo onDidChangeCellExecution = this._onDidChangeCellExecution.event; constructor( - @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @INotebookService private readonly _notebookService: INotebookService, @@ -91,17 +89,12 @@ export class NotebookExecutionStateService extends Disposable implements INotebo this._onDidChangeCellExecution.fire(new NotebookExecutionEvent(notebookUri, cellHandle)); } - createCellExecution(controllerId: string, notebookUri: URI, cellHandle: number): INotebookCellExecution { + createCellExecution(notebookUri: URI, cellHandle: number): INotebookCellExecution { const notebook = this._notebookService.getNotebookTextModel(notebookUri); if (!notebook) { throw new Error(`Notebook not found: ${notebookUri.toString()}`); } - const kernel = this._notebookKernelService.getMatchingKernel(notebook); - if (!kernel.selected || kernel.selected.id !== controllerId) { - throw new Error(`Kernel is not selected: ${kernel.selected?.id} !== ${controllerId}`); - } - let notebookExecutionMap = this._executions.get(notebookUri); if (!notebookExecutionMap) { const listeners = this._instantiationService.createInstance(NotebookExecutionListeners, notebookUri); @@ -224,7 +217,7 @@ function updateToEdit(update: ICellExecuteUpdate, cellHandle: number): ICellEdit if (update.editType === CellExecutionUpdateType.Output) { return { editType: CellEditType.Output, - handle: cellHandle, + handle: update.cellHandle, append: update.append, outputs: update.outputs, }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts index 3a14f27d0ff..a069a226017 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts @@ -4,14 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernel, ISelectedNotebooksChangeEvent, INotebookKernelMatchResult, INotebookKernelService, INotebookTextModelLike } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, ISelectedNotebooksChangeEvent, INotebookKernelMatchResult, INotebookKernelService, INotebookTextModelLike, ISourceAction } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { LRUCache, ResourceMap } from 'vs/base/common/map'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { URI } from 'vs/base/common/uri'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { runWhenIdle } from 'vs/base/common/async'; +import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IAction } from 'vs/base/common/actions'; class KernelInfo { @@ -43,31 +46,64 @@ class NotebookTextModelLikeId { } } +class SourceAction extends Disposable implements ISourceAction { + execution: Promise<void> | undefined; + private readonly _onDidChangeState = this._register(new Emitter<void>()); + readonly onDidChangeState = this._onDidChangeState.event; + + constructor( + readonly action: IAction, + ) { + super(); + } + + async runAction() { + if (this.execution) { + return this.execution; + } + + this.execution = this._runAction(); + this._onDidChangeState.fire(); + await this.execution; + this.execution = undefined; + this._onDidChangeState.fire(); + } + + private async _runAction(): Promise<void> { + await this.action.run(); + } +} + export class NotebookKernelService extends Disposable implements INotebookKernelService { declare _serviceBrand: undefined; private readonly _kernels = new Map<string, KernelInfo>(); - private readonly _typeBindings = new LRUCache<string, string>(100, 0.7); private readonly _notebookBindings = new LRUCache<string, string>(1000, 0.7); private readonly _onDidChangeNotebookKernelBinding = this._register(new Emitter<ISelectedNotebooksChangeEvent>()); private readonly _onDidAddKernel = this._register(new Emitter<INotebookKernel>()); private readonly _onDidRemoveKernel = this._register(new Emitter<INotebookKernel>()); private readonly _onDidChangeNotebookAffinity = this._register(new Emitter<void>()); + private readonly _onDidChangeSourceActions = this._register(new Emitter<void>()); + private readonly _sourceMenu: IMenu; + private _sourceActions: [ISourceAction, IDisposable][]; readonly onDidChangeSelectedNotebooks: Event<ISelectedNotebooksChangeEvent> = this._onDidChangeNotebookKernelBinding.event; readonly onDidAddKernel: Event<INotebookKernel> = this._onDidAddKernel.event; readonly onDidRemoveKernel: Event<INotebookKernel> = this._onDidRemoveKernel.event; readonly onDidChangeNotebookAffinity: Event<void> = this._onDidChangeNotebookAffinity.event; + readonly onDidChangeSourceActions: Event<void> = this._onDidChangeSourceActions.event; private static _storageNotebookBinding = 'notebook.controller2NotebookBindings'; - private static _storageTypeBinding = 'notebook.controller2TypeBindings'; + constructor( @INotebookService private readonly _notebookService: INotebookService, @IStorageService private readonly _storageService: IStorageService, + @IMenuService readonly _menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(); @@ -77,9 +113,13 @@ export class NotebookKernelService extends Disposable implements INotebookKernel this._register(_notebookService.onWillRemoveNotebookDocument(notebook => { const kernelId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook)); if (kernelId) { - this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel: kernelId, newKernel: undefined }); + this.selectKernelForNotebook(undefined, notebook); } })); + this._sourceMenu = this._register(this._menuService.createMenu(MenuId.NotebookKernelSource, contextKeyService)); + this._sourceActions = []; + + this._initSourceActions(); // restore from storage try { @@ -88,16 +128,35 @@ export class NotebookKernelService extends Disposable implements INotebookKernel } catch { // ignore } - try { - const data = JSON.parse(this._storageService.get(NotebookKernelService._storageTypeBinding, StorageScope.GLOBAL, '[]')); - this._typeBindings.fromJSON(data); - } catch { - // ignore - } + } + + private _initSourceActions() { + const loadActionsFromMenu = (menu: IMenu) => { + const groups = menu.getActions({ shouldForwardArgs: true }); + const actions: IAction[] = []; + groups.forEach(group => { + actions.push(...group[1]); + }); + this._sourceActions = actions.map(action => { + const sourceAction = new SourceAction(action); + const stateChangeListener = sourceAction.onDidChangeState(() => { + this._onDidChangeSourceActions.fire(); + }); + return [sourceAction, stateChangeListener]; + }); + this._onDidChangeSourceActions.fire(); + }; + + this._register(this._sourceMenu.onDidChange(() => { + loadActionsFromMenu(this._sourceMenu); + })); + + loadActionsFromMenu(this._sourceMenu); } override dispose() { this._kernels.clear(); + dispose(this._sourceActions.map(a => a[1])); super.dispose(); } @@ -107,7 +166,6 @@ export class NotebookKernelService extends Disposable implements INotebookKernel this._persistSoonHandle?.dispose(); this._persistSoonHandle = runWhenIdle(() => { this._storageService.store(NotebookKernelService._storageNotebookBinding, JSON.stringify(this._notebookBindings), StorageScope.WORKSPACE, StorageTarget.MACHINE); - this._storageService.store(NotebookKernelService._storageTypeBinding, JSON.stringify(this._typeBindings), StorageScope.GLOBAL, StorageTarget.USER); }, 100); } @@ -167,7 +225,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel getMatchingKernel(notebook: INotebookTextModelLike): INotebookKernelMatchResult { // all applicable kernels - const kernels: { kernel: INotebookKernel; instanceAffinity: number; typeAffinity: number; score: number }[] = []; + const kernels: { kernel: INotebookKernel; instanceAffinity: number; score: number }[] = []; for (const info of this._kernels.values()) { const score = NotebookKernelService._score(info.kernel, notebook); if (score) { @@ -175,13 +233,12 @@ export class NotebookKernelService extends Disposable implements INotebookKernel score, kernel: info.kernel, instanceAffinity: info.notebookPriorities.get(notebook.uri) ?? 1 /* vscode.NotebookControllerPriority.Default */, - typeAffinity: this._typeBindings.get(info.kernel.viewType) === info.kernel.id ? 1 : 0 }); } } kernels - .sort((a, b) => b.instanceAffinity - a.instanceAffinity || b.typeAffinity - a.typeAffinity || a.score - b.score || a.kernel.label.localeCompare(b.kernel.label)); + .sort((a, b) => b.instanceAffinity - a.instanceAffinity || a.score - b.score || a.kernel.label.localeCompare(b.kernel.label)); const all = kernels.map(obj => obj.kernel); // bound kernel @@ -208,19 +265,9 @@ export class NotebookKernelService extends Disposable implements INotebookKernel return info.all.length === 1 ? info.all[0] : undefined; } - // default kernel for notebookType - selectKernelForNotebookType(kernel: INotebookKernel, typeId: string): void { - const existing = this._typeBindings.get(typeId); - if (existing !== kernel.id) { - this._typeBindings.set(typeId, kernel.id); - this._persistMementos(); - this._onDidChangeNotebookAffinity.fire(); - } - } - // a notebook has one kernel, a kernel has N notebooks // notebook <-1----N-> kernel - selectKernelForNotebook(kernel: INotebookKernel, notebook: INotebookTextModelLike): void { + selectKernelForNotebook(kernel: INotebookKernel | undefined, notebook: INotebookTextModelLike): void { const key = NotebookTextModelLikeId.str(notebook); const oldKernel = this._notebookBindings.get(key); if (oldKernel !== kernel?.id) { @@ -229,7 +276,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel } else { this._notebookBindings.delete(key); } - this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel, newKernel: kernel.id }); + this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel, newKernel: kernel?.id }); this._persistMementos(); } } @@ -255,4 +302,12 @@ export class NotebookKernelService extends Disposable implements INotebookKernel } this._onDidChangeNotebookAffinity.fire(); } + + getRunningSourceActions() { + return this._sourceActions.filter(action => action[0].execution).map(action => action[0]); + } + + getSourceActions(): ISourceAction[] { + return this._sourceActions.map(a => a[0]); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 374f1d707a1..e93082dac76 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -63,7 +63,7 @@ export class NotebookProviderInfoStore extends Disposable { super(); this._memento = new Memento(NotebookProviderInfoStore.CUSTOM_EDITORS_STORAGE_ID, storageService); - const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); for (const info of (mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] || []) as NotebookEditorDescriptor[]) { this.add(new NotebookProviderInfo(info)); } @@ -128,13 +128,13 @@ export class NotebookProviderInfoStore extends Disposable { } } - const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); } clearEditorCache() { - const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = []; this._memento.saveMemento(); } @@ -236,12 +236,12 @@ export class NotebookProviderInfoStore extends Disposable { const editorRegistration = this._registerContributionPoint(info); this._contributedEditorDisposables.add(editorRegistration); - const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); return toDisposable(() => { - const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const mementoObject = this._memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); this._memento.saveMemento(); editorRegistration.dispose(); @@ -317,8 +317,8 @@ export class NotebookOutputRendererInfoStore { const enum ReuseOrder { PreviouslySelected = 1 << 8, SameExtensionAsNotebook = 2 << 8, - BuiltIn = 3 << 8, - OtherRenderer = 4 << 8, + OtherRenderer = 3 << 8, + BuiltIn = 4 << 8, } const preferred = notebookProviderInfo && this.preferredMimetype.getValue()[notebookProviderInfo.id]?.[mimeType]; diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl.ts index f592a5f7dc9..19de4fc4458 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl.ts @@ -60,7 +60,7 @@ export class NotebookKeymapService extends Disposable implements INotebookKeymap super(); this.notebookKeymapMemento = new Memento('notebookKeymap', storageService); - this.notebookKeymap = this.notebookKeymapMemento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + this.notebookKeymap = this.notebookKeymapMemento.getMemento(StorageScope.PROFILE, StorageTarget.USER); this._register(lifecycleService.onDidShutdown(() => this.dispose())); this._register(this.instantiationService.invokeFunction(onExtensionChanged)((identifiers => { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts index 43c5852753e..5efe69fbc3d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts @@ -10,6 +10,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import * as languages from 'vs/editor/common/languages'; +import { ColorId } from 'vs/editor/common/encodedTokenAttributes'; import { tokenizeLineToHTML } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { ITextModel } from 'vs/editor/common/model'; import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; @@ -33,8 +34,8 @@ class EditorTextRenderer { const fontWeightVar = '--notebook-editor-font-weight'; const style = `` - + `color: ${colorMap[languages.ColorId.DefaultForeground]};` - + `background-color: ${colorMap[languages.ColorId.DefaultBackground]};` + + `color: ${colorMap[ColorId.DefaultForeground]};` + + `background-color: ${colorMap[ColorId.DefaultBackground]};` + `font-family: var(${fontFamilyVar});` + `font-weight: var(${fontWeightVar});` + `font-size: var(${fontSizeVar});` diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index 469e7cbef3d..1d9865937ea 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -9,7 +9,6 @@ import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/no import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; export class CellExecutionPart extends CellPart { private kernelDisposables = this._register(new DisposableStore()); @@ -42,7 +41,7 @@ export class CellExecutionPart extends CellPart { } private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata): void { - if (this._notebookEditor.activeKernel?.type === NotebookKernelType.Resolved && this._notebookEditor.activeKernel?.implementsExecutionOrder) { + if (this._notebookEditor.activeKernel?.implementsExecutionOrder) { const executionOrderLabel = typeof internalMetadata.executionOrder === 'number' ? `[${internalMetadata.executionOrder}]` : '[ ]'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 7306836ac30..181777ec7d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -92,9 +92,7 @@ export class CellOutputElement extends Disposable { } detach() { - if (this.renderedOutputContainer) { - this.renderedOutputContainer.parentElement?.removeChild(this.renderedOutputContainer); - } + this.renderedOutputContainer?.parentElement?.removeChild(this.renderedOutputContainer); let count = 0; if (this.innerContainer) { @@ -602,7 +600,7 @@ export class CellOutputContainer extends CellPart { }); // exclusive - let reRenderRightBoundary = firstGroupEntries.length + newlyInserted.length; + const reRenderRightBoundary = firstGroupEntries.length + newlyInserted.length; const newlyInsertedEntries = newlyInserted.map(insert => { return new OutputEntryViewHandler(insert, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, insert)); @@ -622,7 +620,7 @@ export class CellOutputContainer extends CellPart { entry.element.dispose(); }); - let reRenderRightBoundary = firstGroupEntries.length + newlyInserted.length; + const reRenderRightBoundary = firstGroupEntries.length + newlyInserted.length; const newlyInsertedEntries = newlyInserted.map(insert => { return new OutputEntryViewHandler(insert, this.instantiationService.createInstance(CellOutputElement, this.notebookEditor, this.viewCell, this, this.templateData.outputContainer, insert)); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts index 45fc3cbb235..c84443b70ab 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts @@ -164,9 +164,7 @@ export class CellTitleToolbarPart extends CellPart { if (deferredUpdate && !visible) { this._register(disposableTimeout(() => { - if (deferredUpdate) { - deferredUpdate(); - } + deferredUpdate?.(); })); deferredUpdate = undefined; 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 63cc7be3072..e06c5ff4df9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -257,7 +257,7 @@ export class CodeCell extends Disposable { })); this._register(this.templateData.editor.onDidChangeCursorSelection((e) => { - if (e.source === 'restoreState') { + if (e.source === 'restoreState' || e.oldModelVersionId === 0) { // do not reveal the cell into view if this selection change was caused by restoring editors... return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts index e8b61a303d0..44d8f209cbe 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts @@ -39,7 +39,7 @@ export class FoldedCellHint extends CellPart { const foldHintTop = element.layoutInfo.previewHeight; this._container.style.top = `${foldHintTop}px`; - } else if (element.foldingState === CellFoldingState.Expanded) { + } else { DOM.hide(this._container); } } 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 67cdfaf7002..870d4b2d719 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts @@ -435,9 +435,7 @@ export class StatefulMarkdownCell extends Disposable { updateEditorOptions(newValue: IEditorOptions): void { this.editorOptions = newValue; - if (this.editor) { - this.editor.updateOptions(this.editorOptions); - } + this.editor?.updateOptions(this.editorOptions); } private layoutFoldingIndicator() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 0318aa6a288..b62e89274d0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -39,7 +39,8 @@ const enum CellRevealType { const enum CellRevealPosition { Top, Center, - Bottom + Bottom, + NearTop } function getVisibleCells(cells: CellViewModel[], hiddenRanges: ICellRange[]) { @@ -72,10 +73,6 @@ export interface IFocusNextPreviousDelegate { onFocusPrevious(applyFocusPrevious: () => void): void; } -export interface INotebookCellListOptions extends IWorkbenchListOptions<CellViewModel> { - focusNextPreviousDelegate: IFocusNextPreviousDelegate; -} - export const NOTEBOOK_WEBVIEW_BOUNDARY = 5000; function validateWebviewBoundary(element: HTMLElement) { @@ -140,8 +137,6 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID private _isInLayout: boolean = false; - private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate; - private readonly _viewContext: ViewContext; private _webviewElement: FastDomNode<HTMLElement> | null = null; @@ -158,7 +153,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID delegate: IListVirtualDelegate<CellViewModel>, renderers: IListRenderer<CellViewModel, BaseCellRenderTemplate>[], contextKeyService: IContextKeyService, - options: INotebookCellListOptions, + options: IWorkbenchListOptions<CellViewModel>, @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @@ -167,7 +162,6 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService).set(true); this._viewContext = viewContext; - this._focusNextPreviousDelegate = options.focusNextPreviousDelegate; this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { this._previousFocusedElements.forEach(element => { @@ -627,6 +621,10 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID } const modelIndex = this._viewModel.getCellIndex(cell); + if (modelIndex === -1) { + return -1; + } + if (!this.hiddenRangesPrefixSum) { return modelIndex; } @@ -680,18 +678,6 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID this.setSelection(indices); } - override focusNext(n: number | undefined, loop: boolean | undefined, browserEvent?: UIEvent, filter?: (element: CellViewModel) => boolean): void { - this._focusNextPreviousDelegate.onFocusNext(() => { - super.focusNext(n, loop, browserEvent, filter); - }); - } - - override focusPrevious(n: number | undefined, loop: boolean | undefined, browserEvent?: UIEvent, filter?: (element: CellViewModel) => boolean): void { - this._focusNextPreviousDelegate.onFocusPrevious(() => { - super.focusPrevious(n, loop, browserEvent, filter); - }); - } - override setFocus(indexes: number[], browserEvent?: UIEvent, ignoreTextModelUpdate?: boolean): void { if (ignoreTextModelUpdate) { super.setFocus(indexes, browserEvent); @@ -861,7 +847,15 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID const index = this._getViewIndexUpperBound(cell); if (index >= 0) { - return this._revealInCenterIfOutsideViewportAsync(index); + return this._revealIfOutsideViewportAsync(index, CellRevealPosition.Center); + } + } + + async revealNearTopIfOutsideViewportAync(cell: ICellViewModel): Promise<void> { + const index = this._getViewIndexUpperBound(cell); + + if (index >= 0) { + return this._revealIfOutsideViewportAsync(index, CellRevealPosition.NearTop); } } @@ -965,7 +959,6 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID // update element above viewport const oldHeight = this.elementHeight(element); const delta = oldHeight - size; - // const date = new Date(); if (this._webviewElement) { Event.once(this.view.onWillScroll)(() => { const webviewTop = parseInt(this._webviewElement!.domNode.style.top, 10); @@ -1200,8 +1193,8 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID } } - private async _revealInCenterIfOutsideViewportAsync(viewIndex: number): Promise<void> { - this._revealInternal(viewIndex, true, CellRevealPosition.Center); + private async _revealIfOutsideViewportAsync(viewIndex: number, revealPosition: CellRevealPosition): Promise<void> { + this._revealInternal(viewIndex, true, revealPosition); const element = this.view.element(viewIndex); // wait for the editor to be created only if the cell is in editing mode (meaning it has an editor and will focus the editor) @@ -1232,7 +1225,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID if (ignoreIfInsideViewport && elementTop >= scrollTop - && elementTop < wrapperBottom) { + && elementBottom < wrapperBottom) { if (revealPosition === CellRevealPosition.Center && elementBottom > wrapperBottom @@ -1249,6 +1242,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID this.view.setScrollTop(this.view.elementTop(viewIndex)); break; case CellRevealPosition.Center: + case CellRevealPosition.NearTop: { // reveal the cell top in the viewport center initially this.view.setScrollTop(elementTop - this.view.renderHeight / 2); @@ -1259,8 +1253,10 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID if (newElementHeight >= renderHeight) { // cell is larger than viewport, reveal top this.view.setScrollTop(newElementTop); - } else { + } else if (revealPosition === CellRevealPosition.Center) { this.view.setScrollTop(newElementTop + (newElementHeight / 2) - (renderHeight / 2)); + } else if (revealPosition === CellRevealPosition.NearTop) { + this.view.setScrollTop(newElementTop - (renderHeight / 5)); } } break; @@ -1275,7 +1271,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID private _revealInView(viewIndex: number) { const firstIndex = this.view.firstVisibleIndex; - if (viewIndex < firstIndex) { + if (viewIndex <= firstIndex) { this._revealInternal(viewIndex, true, CellRevealPosition.Top); } else { this._revealInternal(viewIndex, true, CellRevealPosition.Bottom); @@ -1497,6 +1493,10 @@ export class ListViewInfoAccessor extends Disposable { this.list.revealElementInCenter(cell); } + async revealNearTopIfOutsideViewportAync(cell: ICellViewModel) { + return this.list.revealNearTopIfOutsideViewportAync(cell); + } + async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise<void> { return this.list.revealElementLineInViewAsync(cell, line); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index 0bdb4ff330f..ceabb8d6de9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -66,6 +66,7 @@ export interface INotebookCellList { revealElementInCenterIfOutsideViewport(element: ICellViewModel): void; revealElementInCenter(element: ICellViewModel): void; revealElementInCenterIfOutsideViewportAsync(element: ICellViewModel): Promise<void>; + revealNearTopIfOutsideViewportAync(element: ICellViewModel): Promise<void>; revealElementLineInViewAsync(element: ICellViewModel, line: number): Promise<void>; revealElementLineInCenterAsync(element: ICellViewModel, line: number): Promise<void>; revealElementLineInCenterIfOutsideViewportAsync(element: ICellViewModel, line: number): Promise<void>; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index af4606ffee6..29537af1c02 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -37,7 +37,7 @@ import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebo import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernel, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewElement, IWebviewService, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; @@ -75,6 +75,7 @@ export interface INotebookDelegateForWebview { didDragMarkupCell(cellId: string, event: { dragOffsetY: number }): void; didDropMarkupCell(cellId: string, event: { dragOffsetY: number; ctrlKey: boolean; altKey: boolean }): void; didEndDragMarkupCell(cellId: string): void; + didResizeOutput(cellId: string): void; setScrollTop(scrollTop: number): void; triggerScroll(event: IMouseWheelEvent): void; } @@ -818,6 +819,10 @@ var requirejs = (function() { this._handleHighlightCodeBlock(data.codeBlocks); break; } + + case 'outputResized': + this.notebookEditor.didResizeOutput(data.cellId); + break; } })); } @@ -852,7 +857,9 @@ var requirejs = (function() { return; } - const defaultDir = dirname(this.documentUri); + const defaultDir = this.documentUri.scheme === Schemas.vscodeInteractive ? + this.workspaceContextService.getWorkspace().folders[0]?.uri ?? await this.fileDialogService.defaultFilePath() : + dirname(this.documentUri); let defaultName: string; if (event.downloadName) { defaultName = event.downloadName; @@ -912,7 +919,7 @@ var requirejs = (function() { } this._preloadsCache.clear(); - if (this._currentKernel?.type === NotebookKernelType.Resolved) { + if (this._currentKernel) { this._updatePreloadsFromKernel(this._currentKernel); } @@ -1329,12 +1336,10 @@ var requirejs = (function() { this.webview?.focus(); } - setTimeout(() => { // Need this, or focus decoration is not shown. No clue. - this._sendMessageToWebview({ - type: 'focus-output', - cellId, - }); - }, 50); + this._sendMessageToWebview({ + type: 'focus-output', + cellId, + }); } async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean }): Promise<IFindMatch[]> { @@ -1412,14 +1417,14 @@ var requirejs = (function() { const previousKernel = this._currentKernel; this._currentKernel = kernel; - if (previousKernel?.type === NotebookKernelType.Resolved && previousKernel.preloadUris.length > 0) { + if (previousKernel && previousKernel.preloadUris.length > 0) { this.webview?.reload(); // preloads will be restored after reload - } else if (kernel?.type === NotebookKernelType.Resolved) { + } else if (kernel) { this._updatePreloadsFromKernel(kernel); } } - private _updatePreloadsFromKernel(kernel: IResolvedNotebookKernel) { + private _updatePreloadsFromKernel(kernel: INotebookKernel) { const resources: IControllerPreload[] = []; for (const preload of kernel.preloadUris) { const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https') @@ -1445,7 +1450,7 @@ var requirejs = (function() { const mixedResourceRoots = [ ...(this.localResourceRootsCache || []), - ...(this._currentKernel?.type === NotebookKernelType.Resolved ? [this._currentKernel.localResourceRoot] : []), + ...(this._currentKernel ? [this._currentKernel.localResourceRoot] : []), ]; this.webview.localResourcesRoot = mixedResourceRoots; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 8b98bba83f8..4888d56bd8f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -401,6 +401,12 @@ export interface IDidFindHighlightMessage extends BaseToWebviewMessage { readonly offset: number; } +export interface IOutputResizedMessage extends BaseToWebviewMessage { + readonly type: 'outputResized'; + readonly cellId: string; +} + + export type FromWebviewMessage = WebviewInitialized | IDimensionMessage | IMouseEnterMessage | @@ -428,7 +434,8 @@ export type FromWebviewMessage = WebviewInitialized | IRenderedMarkupMessage | IRenderedCellOutputMessage | IDidFindMessage | - IDidFindHighlightMessage; + IDidFindHighlightMessage | + IOutputResizedMessage; export type ToWebviewMessage = IClearMessage | IFocusOutputMessage | @@ -460,4 +467,5 @@ export type ToWebviewMessage = IClearMessage | IFindUnHighlightMessage | IFindStopMessage; + export type AnyMessage = FromWebviewMessage | ToWebviewMessage; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 33155f8dc15..0ce36982b24 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -5,10 +5,15 @@ import type { Event } from 'vs/base/common/event'; import type { IDisposable } from 'vs/base/common/lifecycle'; -import { RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import type * as webviewMessages from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages'; import type * as rendererApi from 'vscode-notebook-renderer'; +// !! IMPORTANT !! ---------------------------------------------------------------------------------- +// import { RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +// We can ONLY IMPORT as type in this module. This also applies to const enums that would evaporate +// in normal compiles but remain a dependency in transpile-only compiles +// !! IMPORTANT !! ---------------------------------------------------------------------------------- + // !! IMPORTANT !! everything must be in-line within the webviewPreloads // function. Imports are not allowed. This is stringified and injected into // the webview. @@ -75,7 +80,7 @@ async function webviewPreloads(ctx: PreloadContext) { let currentOptions = ctx.options; let isWorkspaceTrusted = ctx.isWorkspaceTrusted; - let lineLimit = ctx.lineLimit; + const lineLimit = ctx.lineLimit; const acquireVsCodeApi = globalThis.acquireVsCodeApi; const vscode = acquireVsCodeApi(); @@ -294,7 +299,8 @@ async function webviewPreloads(ctx: PreloadContext) { private readonly _observer: ResizeObserver; - private readonly _observedElements = new WeakMap<Element, { id: string; output: boolean; lastKnownHeight: number }>(); + private readonly _observedElements = new WeakMap<Element, { id: string; output: boolean; lastKnownHeight: number; cellId: string }>(); + private _outputResizeTimer: any; constructor() { this._observer = new ResizeObserver(entries => { @@ -308,6 +314,8 @@ async function webviewPreloads(ctx: PreloadContext) { continue; } + this.postResizeMessage(observedElementInfo.cellId); + if (entry.target.id === observedElementInfo.id && entry.contentRect) { if (observedElementInfo.output) { if (entry.contentRect.height !== 0) { @@ -329,14 +337,26 @@ async function webviewPreloads(ctx: PreloadContext) { }); } - public observe(container: Element, id: string, output: boolean) { + public observe(container: Element, id: string, output: boolean, cellId: string) { if (this._observedElements.has(container)) { return; } - this._observedElements.set(container, { id, output, lastKnownHeight: -1 }); + this._observedElements.set(container, { id, output, lastKnownHeight: -1, cellId }); this._observer.observe(container); } + + private postResizeMessage(cellId: string) { + // Debounce this callback to only happen after + // 250 ms. Don't need resize events that often. + clearTimeout(this._outputResizeTimer); + this._outputResizeTimer = setTimeout(() => { + postNotebookMessage('outputResized', { + cellId + }); + }, 250); + + } }; function scrollWillGoToParent(event: WheelEvent) { @@ -387,6 +407,7 @@ async function webviewPreloads(ctx: PreloadContext) { function createFocusSink(cellId: string, focusNext?: boolean) { const element = document.createElement('div'); + element.id = `focus-sink-${cellId}`; element.tabIndex = 0; element.addEventListener('focus', () => { postNotebookMessage<webviewMessages.IBlurOutputMessage>('focus-editor', { @@ -738,8 +759,8 @@ async function webviewPreloads(ctx: PreloadContext) { } let _highlighter: IHighlighter | null = null; - let matchColor = window.getComputedStyle(document.getElementById('_defaultColorPalatte')!).color; - let currentMatchColor = window.getComputedStyle(document.getElementById('_defaultColorPalatte')!).backgroundColor; + const matchColor = window.getComputedStyle(document.getElementById('_defaultColorPalatte')!).color; + const currentMatchColor = window.getComputedStyle(document.getElementById('_defaultColorPalatte')!).backgroundColor; class JSHighlighter implements IHighlighter { private _findMatchIndex = -1; @@ -760,9 +781,7 @@ async function webviewPreloads(ctx: PreloadContext) { highlightCurrentMatch(index: number) { const oldMatch = this.matches[this._findMatchIndex]; - if (oldMatch) { - oldMatch.highlightResult?.update(matchColor, oldMatch.isShadow ? undefined : 'find-match'); - } + oldMatch?.highlightResult?.update(matchColor, oldMatch.isShadow ? undefined : 'find-match'); const match = this.matches[index]; this._findMatchIndex = index; @@ -859,11 +878,11 @@ async function webviewPreloads(ctx: PreloadContext) { const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean }) => { let find = true; - let matches: IFindMatch[] = []; + const matches: IFindMatch[] = []; - let range = document.createRange(); + const range = document.createRange(); range.selectNodeContents(document.getElementById('findStart')!); - let sel = window.getSelection(); + const sel = window.getSelection(); sel?.removeAllRanges(); sel?.addRange(range); @@ -1512,7 +1531,7 @@ async function webviewPreloads(ctx: PreloadContext) { } const cellOutput = this.ensureOutputCell(data.cellId, data.cellTop, false); - const outputNode = cellOutput.createOutputElement(data.outputId, data.outputOffset, data.left); + const outputNode = cellOutput.createOutputElement(data.outputId, data.outputOffset, data.left, data.cellId); outputNode.render(data.content, preloadsAndErrors); // don't hide until after this step so that the height is right @@ -1665,7 +1684,7 @@ async function webviewPreloads(ctx: PreloadContext) { this.addEventListeners(); this.updateContentAndRender(this._content.value).then(() => { - resizeObserver.observe(this.element, this.id, false); + resizeObserver.observe(this.element, this.id, false, this.id); resolveReady(); }); } @@ -1799,7 +1818,6 @@ async function webviewPreloads(ctx: PreloadContext) { class OutputCell { public readonly element: HTMLElement; - private readonly outputElements = new Map</*outputId*/ string, OutputContainer>(); constructor(cellId: string) { @@ -1821,7 +1839,7 @@ async function webviewPreloads(ctx: PreloadContext) { container.appendChild(lowerWrapperElement); } - public createOutputElement(outputId: string, outputOffset: number, left: number): OutputElement { + public createOutputElement(outputId: string, outputOffset: number, left: number, cellId: string): OutputElement { let outputContainer = this.outputElements.get(outputId); if (!outputContainer) { outputContainer = new OutputContainer(outputId); @@ -1829,7 +1847,7 @@ async function webviewPreloads(ctx: PreloadContext) { this.outputElements.set(outputId, outputContainer); } - return outputContainer.createOutputElement(outputId, outputOffset, left); + return outputContainer.createOutputElement(outputId, outputOffset, left, cellId); } public clearOutput(outputId: string, rendererId: string | undefined) { @@ -1911,12 +1929,12 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.top = `${outputOffset}px`; } - public createOutputElement(outputId: string, outputOffset: number, left: number): OutputElement { + public createOutputElement(outputId: string, outputOffset: number, left: number, cellId: string): OutputElement { this.element.innerText = ''; this.element.style.maxHeight = '0px'; this.element.style.top = `${outputOffset}px`; - this._outputNode = new OutputElement(outputId, left); + this._outputNode = new OutputElement(outputId, left, cellId); this.element.appendChild(this._outputNode.element); return this._outputNode; } @@ -1948,13 +1966,13 @@ async function webviewPreloads(ctx: PreloadContext) { class OutputElement { public readonly element: HTMLElement; - private _content?: { content: webviewMessages.ICreationContent; preloadsAndErrors: unknown[] }; private hasResizeObserver = false; constructor( private readonly outputId: string, left: number, + public readonly cellId: string ) { this.element = document.createElement('div'); this.element.id = outputId; @@ -1970,7 +1988,7 @@ async function webviewPreloads(ctx: PreloadContext) { public render(content: webviewMessages.ICreationContent, preloadsAndErrors: unknown[]) { this._content = { content, preloadsAndErrors }; - if (content.type === RenderOutputType.Html) { + if (content.type === 0 /* RenderOutputType.Html */) { const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent; this.element.innerHTML = trustedHtml as string; domEval(this.element); @@ -1988,7 +2006,7 @@ async function webviewPreloads(ctx: PreloadContext) { if (!this.hasResizeObserver) { this.hasResizeObserver = true; - resizeObserver.observe(this.element, this.outputId, true); + resizeObserver.observe(this.element, this.outputId, true, this.cellId); } const offsetHeight = this.element.offsetHeight; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index d602602277f..b41c7a096f5 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 _isDisposed = false; + constructor( readonly viewType: string, readonly model: NotebookCellTextModel, @@ -550,6 +552,10 @@ export abstract class BaseCellViewModel extends Disposable { async resolveTextModel(): Promise<model.ITextModel> { if (!this._textModelRef || !this.textModel) { this._textModelRef = await this._modelService.createModelReference(this.uri); + if (this._isDisposed) { + return this.textModel!; + } + if (!this._textModelRef) { throw new Error(`Cannot resolve text model for ${this.uri}`); } @@ -590,6 +596,7 @@ export abstract class BaseCellViewModel extends Disposable { } override dispose() { + this._isDisposed = true; super.dispose(); dispose(this._editorListeners); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorDecorations.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorDecorations.ts deleted file mode 100644 index fbdba52e8a8..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorDecorations.ts +++ /dev/null @@ -1,205 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as DOM from 'vs/base/browser/dom'; -import { URI } from 'vs/base/common/uri'; -import * as strings from 'vs/base/common/strings'; -import { IContentDecorationRenderOptions, isThemeColor } from 'vs/editor/common/editorCommon'; -import { IColorTheme, IThemeService, ThemeColor } from 'vs/platform/theme/common/themeService'; -import { INotebookDecorationRenderOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { _CSS_MAP } from 'vs/editor/browser/services/abstractCodeEditorService'; - -export class NotebookRefCountedStyleSheet { - private readonly _key: string; - private readonly _styleSheet: HTMLStyleElement; - private _refCount: number; - - constructor(readonly widget: { removeEditorStyleSheets: (key: string) => void }, key: string, styleSheet: HTMLStyleElement) { - this._key = key; - this._styleSheet = styleSheet; - this._refCount = 0; - } - - public ref(): void { - this._refCount++; - } - - public unref(): void { - this._refCount--; - if (this._refCount === 0) { - this._styleSheet.parentNode?.removeChild(this._styleSheet); - this.widget.removeEditorStyleSheets(this._key); - } - } - - public insertRule(rule: string, index?: number): void { - const sheet = <CSSStyleSheet>this._styleSheet.sheet; - sheet.insertRule(rule, index); - } -} - -interface ProviderArguments { - styleSheet: NotebookRefCountedStyleSheet; - key: string; - options: INotebookDecorationRenderOptions; -} - -export class NotebookDecorationCSSRules { - private _theme: IColorTheme; - private _className: string; - private _topClassName: string; - - get className() { - return this._className; - } - - get topClassName() { - return this._topClassName; - } - - constructor( - private readonly _themeService: IThemeService, - private readonly _styleSheet: NotebookRefCountedStyleSheet, - private readonly _providerArgs: ProviderArguments - ) { - this._styleSheet.ref(); - this._theme = this._themeService.getColorTheme(); - this._className = CSSNameHelper.getClassName(this._providerArgs.key, CellDecorationCSSRuleType.ClassName); - this._topClassName = CSSNameHelper.getClassName(this._providerArgs.key, CellDecorationCSSRuleType.TopClassName); - this._buildCSS(); - } - - private _buildCSS() { - if (this._providerArgs.options.backgroundColor) { - const backgroundColor = this._resolveValue(this._providerArgs.options.backgroundColor); - this._styleSheet.insertRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.${this.className} .cell-focus-indicator, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row.${this.className} { - background-color: ${backgroundColor} !important; - }`); - } - - if (this._providerArgs.options.borderColor) { - const borderColor = this._resolveValue(this._providerArgs.options.borderColor); - - this._styleSheet.insertRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.${this.className} .cell-focus-indicator-top:before, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.${this.className} .cell-focus-indicator-bottom:before { - border-color: ${borderColor} !important; - }`); - - this._styleSheet.insertRule(` - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.${this.className} .cell-focus-indicator-bottom:before { - content: ""; - position: absolute; - width: 100%; - height: 1px; - border-bottom: 1px solid ${borderColor}; - bottom: 0px; - `); - - this._styleSheet.insertRule(` - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.${this.className} .cell-focus-indicator-top:before { - content: ""; - position: absolute; - width: 100%; - height: 1px; - border-top: 1px solid ${borderColor}; - `); - - // more specific rule for `.focused` can override existing rules - this._styleSheet.insertRule(`.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused.${this.className} .cell-focus-indicator-top:before, - .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused.${this.className} .cell-focus-indicator-bottom:before { - border-color: ${borderColor} !important; - }`); - } - - if (this._providerArgs.options.top) { - const unthemedCSS = this._getCSSTextForModelDecorationContentClassName(this._providerArgs.options.top); - this._styleSheet.insertRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.${this.className} .cell-decoration .${this.topClassName} { - height: 1rem; - display: block; - }`); - - this._styleSheet.insertRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.${this.className} .cell-decoration .${this.topClassName}::before { - display: block; - ${unthemedCSS} - }`); - } - } - - /** - * Build the CSS for decorations styled before or after content. - */ - private _getCSSTextForModelDecorationContentClassName(opts: IContentDecorationRenderOptions | undefined): string { - if (!opts) { - return ''; - } - const cssTextArr: string[] = []; - - if (typeof opts !== 'undefined') { - this._collectBorderSettingsCSSText(opts, cssTextArr); - if (typeof opts.contentIconPath !== 'undefined') { - cssTextArr.push(strings.format(_CSS_MAP.contentIconPath, DOM.asCSSUrl(URI.revive(opts.contentIconPath)))); - } - if (typeof opts.contentText === 'string') { - const truncated = opts.contentText.match(/^.*$/m)![0]; // only take first line - const escaped = truncated.replace(/['\\]/g, '\\$&'); - - cssTextArr.push(strings.format(_CSS_MAP.contentText, escaped)); - } - this._collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin'], cssTextArr); - if (this._collectCSSText(opts, ['width', 'height'], cssTextArr)) { - cssTextArr.push('display:inline-block;'); - } - } - - return cssTextArr.join(''); - } - - private _collectBorderSettingsCSSText(opts: any, cssTextArr: string[]): boolean { - if (this._collectCSSText(opts, ['border', 'borderColor', 'borderRadius', 'borderSpacing', 'borderStyle', 'borderWidth'], cssTextArr)) { - cssTextArr.push(strings.format('box-sizing: border-box;')); - return true; - } - return false; - } - - private _collectCSSText(opts: any, properties: string[], cssTextArr: string[]): boolean { - const lenBefore = cssTextArr.length; - for (const property of properties) { - const value = this._resolveValue(opts[property]); - if (typeof value === 'string') { - cssTextArr.push(strings.format(_CSS_MAP[property], value)); - } - } - return cssTextArr.length !== lenBefore; - } - - private _resolveValue(value: string | ThemeColor): string { - if (isThemeColor(value)) { - const color = this._theme.getColor(value.id); - if (color) { - return color.toString(); - } - return 'transparent'; - } - return value; - } - - dispose() { - this._styleSheet.unref(); - } -} - -const enum CellDecorationCSSRuleType { - ClassName = 0, - TopClassName = 0, -} - -class CSSNameHelper { - - public static getClassName(key: string, type: CellDecorationCSSRuleType): string { - return 'nb-' + key + '-' + type; - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 243b4b96edc..1d1d38c2689 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -405,9 +405,7 @@ export class NotebookEditorToolbar extends Disposable { if (deferredUpdate && !visible) { setTimeout(() => { - if (deferredUpdate) { - deferredUpdate(); - } + deferredUpdate?.(); }, 0); deferredUpdate = undefined; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index b1cf70a4946..3702ac26261 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -6,15 +6,16 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICellViewModel, INotebookEditorDelegate, KERNEL_EXTENSIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NOTEBOOK_CELL_TOOLBAR_LOCATION, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { NOTEBOOK_CELL_TOOLBAR_LOCATION, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_KERNEL_SOURCE_COUNT, NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class NotebookEditorContextKeys { private readonly _notebookKernel: IContextKey<string>; private readonly _notebookKernelCount: IContextKey<number>; + private readonly _notebookKernelSourceCount: IContextKey<number>; private readonly _notebookKernelSelected: IContextKey<boolean>; private readonly _interruptibleKernel: IContextKey<boolean>; private readonly _someCellRunning: IContextKey<boolean>; @@ -44,6 +45,7 @@ export class NotebookEditorContextKeys { this._hasOutputs = NOTEBOOK_HAS_OUTPUTS.bindTo(contextKeyService); this._viewType = NOTEBOOK_VIEW_TYPE.bindTo(contextKeyService); this._missingKernelExtension = NOTEBOOK_MISSING_KERNEL_EXTENSION.bindTo(contextKeyService); + this._notebookKernelSourceCount = NOTEBOOK_KERNEL_SOURCE_COUNT.bindTo(contextKeyService); this._cellToolbarLocation = NOTEBOOK_CELL_TOOLBAR_LOCATION.bindTo(contextKeyService); this._handleDidChangeModel(); @@ -52,6 +54,7 @@ export class NotebookEditorContextKeys { this._disposables.add(_editor.onDidChangeModel(this._handleDidChangeModel, this)); this._disposables.add(_notebookKernelService.onDidAddKernel(this._updateKernelContext, this)); this._disposables.add(_notebookKernelService.onDidChangeSelectedNotebooks(this._updateKernelContext, this)); + this._disposables.add(_notebookKernelService.onDidChangeSourceActions(this._updateKernelContext, this)); this._disposables.add(_editor.notebookOptions.onDidChangeOptions(this._updateForNotebookOptions, this)); this._disposables.add(_extensionService.onDidChangeExtensions(this._updateForInstalledExtension, this)); this._disposables.add(_notebookExecutionStateService.onDidChangeCellExecution(this._updateForCellExecution, this)); @@ -61,6 +64,7 @@ export class NotebookEditorContextKeys { this._disposables.dispose(); this._viewModelDisposables.dispose(); this._notebookKernelCount.reset(); + this._notebookKernelSourceCount.reset(); this._interruptibleKernel.reset(); this._someCellRunning.reset(); this._viewType.reset(); @@ -142,13 +146,16 @@ export class NotebookEditorContextKeys { private _updateKernelContext(): void { if (!this._editor.hasModel()) { this._notebookKernelCount.reset(); + this._notebookKernelSourceCount.reset(); this._interruptibleKernel.reset(); return; } const { selected, all } = this._notebookKernelService.getMatchingKernel(this._editor.textModel); + const sourceActions = this._notebookKernelService.getSourceActions(); this._notebookKernelCount.set(all.length); - this._interruptibleKernel.set((selected?.type === NotebookKernelType.Resolved && selected.implementsInterrupt) ?? false); + this._notebookKernelSourceCount.set(sourceActions.length); + this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); this._notebookKernelSelected.set(Boolean(selected)); this._notebookKernel.set(selected?.id ?? ''); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts index e5814ce98ec..958a59df428 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts @@ -6,11 +6,10 @@ import 'vs/css!./notebookKernelActionViewItem'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Action, IAction } from 'vs/base/common/actions'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { INotebookKernelMatchResult, INotebookKernelService, NotebookKernelType, ProxyKernelState } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { executingStateIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { INotebookKernel, INotebookKernelMatchResult, INotebookKernelService, ISourceAction } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { Event } from 'vs/base/common/event'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -18,7 +17,6 @@ import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookB export class NotebooKernelActionViewItem extends ActionViewItem { private _kernelLabel?: HTMLAnchorElement; - private _kernelDisposable: DisposableStore; constructor( actualAction: IAction, @@ -33,7 +31,7 @@ export class NotebooKernelActionViewItem extends ActionViewItem { this._register(_editor.onDidChangeModel(this._update, this)); this._register(_notebookKernelService.onDidChangeNotebookAffinity(this._update, this)); this._register(_notebookKernelService.onDidChangeSelectedNotebooks(this._update, this)); - this._kernelDisposable = this._register(new DisposableStore()); + this._register(_notebookKernelService.onDidChangeSourceActions(this._update, this)); } override render(container: HTMLElement): void { @@ -61,39 +59,51 @@ export class NotebooKernelActionViewItem extends ActionViewItem { return; } + const runningActions = this._notebookKernelService.getRunningSourceActions(); + if (runningActions.length) { + return this._updateActionFromSourceAction(runningActions[0] /** TODO handle multiple actions state */, true); + } + const info = this._notebookKernelService.getMatchingKernel(notebook); + if (info.all.length === 0) { + return this._updateActionsFromSourceActions(); + } + this._updateActionFromKernelInfo(info); } + private _updateActionFromSourceAction(sourceAction: ISourceAction, running: boolean) { + const action = sourceAction.action; + this.action.class = running ? ThemeIcon.asClassName(ThemeIcon.modify(executingStateIcon, 'spin')) : ThemeIcon.asClassName(selectKernelIcon); + this.updateClass(); + this._action.label = action.label; + this._action.enabled = true; + } + + private _updateActionsFromSourceActions() { + this._action.enabled = true; + const sourceActions = this._notebookKernelService.getSourceActions(); + if (sourceActions.length === 1) { + // exact one action + this._updateActionFromSourceAction(sourceActions[0], false); + } else { + this._action.class = ThemeIcon.asClassName(selectKernelIcon); + this._action.label = localize('select', "Select Kernel"); + this._action.tooltip = ''; + } + } + private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { - this._kernelDisposable.clear(); this._action.enabled = true; - const selectedOrSuggested = info.selected ?? ((info.suggestions.length === 1 && info.suggestions[0].type === NotebookKernelType.Resolved) ? info.suggestions[0] : undefined); + this._action.class = ThemeIcon.asClassName(selectKernelIcon); + const selectedOrSuggested = info.selected ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined); if (selectedOrSuggested) { // selected or suggested kernel - this._action.label = selectedOrSuggested.label; + this._action.label = this._generateKenrelLabel(selectedOrSuggested); this._action.tooltip = selectedOrSuggested.description ?? selectedOrSuggested.detail ?? ''; if (!info.selected) { // special UI for selected kernel? } - - if (selectedOrSuggested.type === NotebookKernelType.Proxy) { - if (selectedOrSuggested.connectionState === ProxyKernelState.Initializing) { - this._action.label = localize('initializing', "Initializing..."); - } else { - this._action.label = selectedOrSuggested.label; - } - - this._kernelDisposable.add(selectedOrSuggested.onDidChange(e => { - if (e.connectionState) { - if (selectedOrSuggested.connectionState === ProxyKernelState.Initializing) { - this._action.label = localize('initializing', "Initializing..."); - } else { - this._action.label = selectedOrSuggested.label; - } - } - })); - } } else { // many kernels or no kernels this._action.label = localize('select', "Select Kernel"); @@ -101,6 +111,10 @@ export class NotebooKernelActionViewItem extends ActionViewItem { } } + private _generateKenrelLabel(kernel: INotebookKernel) { + return kernel.label; + } + private _resetAction(): void { this._action.enabled = false; this._action.label = ''; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 28ff3f4f885..e590c5cddb9 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -75,7 +75,7 @@ export const RENDERER_EQUIVALENT_EXTENSIONS: ReadonlyMap<string, ReadonlySet<str export const RENDERER_NOT_AVAILABLE = '_notAvailable'; -export type NotebookRendererEntrypoint = string | { extends: string; path: string }; +export type NotebookRendererEntrypoint = string | { readonly extends: string; readonly path: string }; export enum NotebookRunState { Running = 1, @@ -927,7 +927,8 @@ export const NotebookSetting = { interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode', outputLineHeight: 'notebook.outputLineHeight', outputFontSize: 'notebook.outputFontSize', - outputFontFamily: 'notebook.outputFontFamily' + outputFontFamily: 'notebook.outputFontFamily', + interactiveWindowAlwaysScrollOnNewCell: 'interactiveWindow.alwaysScrollOnNewCell' } as const; export const enum CellStatusbarAlignment { @@ -935,12 +936,6 @@ export const enum CellStatusbarAlignment { Right = 2 } -export interface INotebookDecorationRenderOptions { - backgroundColor?: string | ThemeColor; - borderColor?: string | ThemeColor; - top?: editorCommon.IContentDecorationRenderOptions; -} - export class NotebookWorkingCopyTypeIdentifier { private static _prefix = 'notebook/'; diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 689f9ca995e..e629ed034d2 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -23,6 +23,7 @@ export const NOTEBOOK_HAS_RUNNING_CELL = new RawContextKey<boolean>('notebookHas export const NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON = new RawContextKey<boolean>('notebookUseConsolidatedOutputButton', false); export const NOTEBOOK_BREAKPOINT_MARGIN_ACTIVE = new RawContextKey<boolean>('notebookBreakpointMargin', false); export const NOTEBOOK_CELL_TOOLBAR_LOCATION = new RawContextKey<'left' | 'right' | 'hidden'>('notebookCellToolbarLocation', 'left'); +export const NOTEBOOK_CURSOR_NAVIGATION_MODE = new RawContextKey<boolean>('notebookCursorNavigationMode', false); // Cell keys export const NOTEBOOK_VIEW_TYPE = new RawContextKey<string>('notebookType', undefined); @@ -43,6 +44,7 @@ export const NOTEBOOK_CELL_RESOURCE = new RawContextKey<string>('notebookCellRes // Kernels export const NOTEBOOK_KERNEL = new RawContextKey<string>('notebookKernel', undefined); export const NOTEBOOK_KERNEL_COUNT = new RawContextKey<number>('notebookKernelCount', 0); +export const NOTEBOOK_KERNEL_SOURCE_COUNT = new RawContextKey<number>('notebookKernelSourceCount', 0); export const NOTEBOOK_KERNEL_SELECTED = new RawContextKey<boolean>('notebookKernelSelected', false); export const NOTEBOOK_INTERRUPTIBLE_KERNEL = new RawContextKey<boolean>('notebookInterruptibleKernel', false); export const NOTEBOOK_MISSING_KERNEL_EXTENSION = new RawContextKey<boolean>('notebookMissingKernelExtension', false); diff --git a/src/vs/workbench/contrib/notebook/common/notebookExecutionService.ts b/src/vs/workbench/contrib/notebook/common/notebookExecutionService.ts index f95ed7b6e30..1a3b6ac9a94 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookExecutionService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookExecutionService.ts @@ -15,6 +15,7 @@ export enum CellExecutionUpdateType { export interface ICellExecuteOutputEdit { editType: CellExecutionUpdateType.Output; + cellHandle: number; append?: boolean; outputs: IOutputDto[]; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts index 0a9902a5ff4..f6317245bb1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookExecutionStateService.ts @@ -50,7 +50,7 @@ export interface INotebookExecutionStateService { forceCancelNotebookExecutions(notebookUri: URI): void; getCellExecutionStatesForNotebook(notebook: URI): INotebookCellExecution[]; getCellExecution(cellUri: URI): INotebookCellExecution | undefined; - createCellExecution(controllerId: string, notebook: URI, cellHandle: number): INotebookCellExecution; + createCellExecution(notebook: URI, cellHandle: number): INotebookCellExecution; } export interface INotebookCellExecution { diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index 4f5304600b0..756377bdd86 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -31,13 +32,7 @@ export interface INotebookKernelChangeEvent { hasExecutionOrder?: true; } -export const enum NotebookKernelType { - Resolved, - Proxy = 1 -} - -export interface IResolvedNotebookKernel { - readonly type: NotebookKernelType.Resolved; +export interface INotebookKernel { readonly id: string; readonly viewType: string; readonly onDidChange: Event<Readonly<INotebookKernelChangeEvent>>; @@ -69,24 +64,13 @@ export interface INotebookProxyKernelChangeEvent extends INotebookKernelChangeEv connectionState?: true; } -export interface INotebookProxyKernel { - readonly type: NotebookKernelType.Proxy; - readonly id: string; - readonly viewType: string; - readonly extension: ExtensionIdentifier; - readonly preloadProvides: string[]; - readonly onDidChange: Event<Readonly<INotebookProxyKernelChangeEvent>>; - label: string; - description?: string; - detail?: string; - kind?: string; - supportedLanguages: string[]; - connectionState: ProxyKernelState; - resolveKernel(uri: URI): Promise<string | null>; +export interface ISourceAction { + readonly action: IAction; + readonly onDidChangeState: Event<void>; + execution: Promise<void> | undefined; + runAction: () => Promise<void>; } -export type INotebookKernel = IResolvedNotebookKernel | INotebookProxyKernel; - export interface INotebookTextModelLike { uri: URI; viewType: string } export const INotebookKernelService = createDecorator<INotebookKernelService>('INotebookKernelService'); @@ -98,7 +82,6 @@ export interface INotebookKernelService { readonly onDidRemoveKernel: Event<INotebookKernel>; readonly onDidChangeSelectedNotebooks: Event<ISelectedNotebooksChangeEvent>; readonly onDidChangeNotebookAffinity: Event<void>; - registerKernel(kernel: INotebookKernel): IDisposable; getMatchingKernel(notebook: INotebookTextModelLike): INotebookKernelMatchResult; @@ -120,15 +103,13 @@ export interface INotebookKernelService { preselectKernelForNotebook(kernel: INotebookKernel, notebook: INotebookTextModelLike): void; /** - * Bind a notebook type to a kernel. - * @param viewType - * @param kernel - */ - selectKernelForNotebookType(kernel: INotebookKernel, viewType: string): void; - - /** * Set a perference of a kernel for a certain notebook. Higher values win, `undefined` removes the preference */ updateKernelNotebookAffinity(kernel: INotebookKernel, notebook: URI, preference: number | undefined): void; + //#region Kernel source actions + readonly onDidChangeSourceActions: Event<void>; + getSourceActions(): ISourceAction[]; + getRunningSourceActions(): ISourceAction[]; + //#endregion } diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts index 51b61d425b1..f07e2ac2bec 100644 --- a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts +++ b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts @@ -199,9 +199,7 @@ export class NotebookEditorSimpleWorker implements IRequestHandler, IDisposable public acceptModelChanged(strURL: string, event: NotebookCellsChangedEventDto) { const model = this._models[strURL]; - if (model) { - model.acceptModelChanged(event); - } + model?.acceptModelChanged(event); } public acceptRemovedModel(strURL: string): void { diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts index f4f552c814e..2efc9005e0c 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts @@ -35,7 +35,7 @@ async function testCellDnd(beginning: IBeginningState, dragAction: IDragAction, editor.setFocus({ start: beginning.focus, end: beginning.focus + 1 }); performCellDropEdits(editor, viewModel.cellAt(dragAction.dragIdx)!, dragAction.direction, viewModel.cellAt(dragAction.dragOverIdx)!); - for (let i in end.endOrder) { + for (const i in end.endOrder) { assert.equal(viewModel.viewCells[i].getText(), end.endOrder[i]); } 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 8d3bff7a83a..e249f73080a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -11,6 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { assertThrowsAsync } from 'vs/base/test/common/utils'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { insertCellAtIndex } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; @@ -20,7 +21,7 @@ import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService, IResolvedNotebookKernel, ISelectedNotebooksChangeEvent, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService, ISelectedNotebooksChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -42,6 +43,16 @@ suite('NotebookExecutionService', () => { override getNotebookTextModels() { return []; } }); + instantiationService.stub(IMenuService, new class extends mock<IMenuService>() { + override createMenu() { + return new class extends mock<IMenu>() { + override onDidChange = Event.None; + override getActions() { return []; } + override dispose() { } + }; + } + }); + kernelService = instantiationService.createInstance(NotebookKernelService); instantiationService.set(INotebookKernelService, kernelService); @@ -166,8 +177,7 @@ suite('NotebookExecutionService', () => { }); }); -class TestNotebookKernel implements IResolvedNotebookKernel { - type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; +class TestNotebookKernel implements INotebookKernel { id: string = 'test'; label: string = ''; viewType = '*'; @@ -185,8 +195,10 @@ class TestNotebookKernel implements IResolvedNotebookKernel { cancelNotebookCellExecution(): Promise<void> { throw new Error('Method not implemented.'); } - constructor(opts?: { languages: string[] }) { this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID]; } + kind?: string | undefined; + implementsInterrupt?: boolean | undefined; + implementsExecutionOrder?: boolean | undefined; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts index e5b7f84b8bf..e925ef9e011 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -10,6 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { insertCellAtIndex } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; @@ -21,7 +22,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CellEditType, CellKind, CellUri, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -47,6 +48,16 @@ suite('NotebookExecutionStateService', () => { } }); + instantiationService.stub(IMenuService, new class extends mock<IMenuService>() { + override createMenu() { + return new class extends mock<IMenu>() { + override onDidChange = Event.None; + override getActions() { return []; } + override dispose() { } + }; + } + }); + kernelService = instantiationService.createInstance(NotebookKernelService); instantiationService.set(INotebookKernelService, kernelService); instantiationService.set(INotebookExecutionService, instantiationService.createInstance(NotebookExecutionService)); @@ -83,7 +94,7 @@ suite('NotebookExecutionStateService', () => { const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - executionStateService.createCellExecution(kernel.id, viewModel.uri, cell.handle); + executionStateService.createCellExecution(viewModel.uri, cell.handle); assert.strictEqual(didCancel, false); viewModel.notebookDocument.applyEdits([{ editType: CellEditType.Replace, index: 0, count: 1, cells: [] @@ -102,7 +113,7 @@ suite('NotebookExecutionStateService', () => { const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - const exe = executionStateService.createCellExecution(kernel.id, viewModel.uri, cell.handle); + const exe = executionStateService.createCellExecution(viewModel.uri, cell.handle); let didFire = false; disposables.add(executionStateService.onDidChangeCellExecution(e => { @@ -143,7 +154,7 @@ suite('NotebookExecutionStateService', () => { deferred.complete(); })); - executionStateService.createCellExecution(kernel.id, viewModel.uri, cell.handle); + executionStateService.createCellExecution(viewModel.uri, cell.handle); return deferred.p; }); @@ -159,7 +170,7 @@ suite('NotebookExecutionStateService', () => { const executionStateService: INotebookExecutionStateService = instantiationService.get(INotebookExecutionStateService); const cell = insertCellAtIndex(viewModel, 0, 'var c = 3', 'javascript', CellKind.Code, {}, [], true, true); - executionStateService.createCellExecution(kernel.id, viewModel.uri, cell.handle); + executionStateService.createCellExecution(viewModel.uri, cell.handle); const exe = executionStateService.getCellExecution(cell.uri); assert.ok(exe); @@ -170,8 +181,7 @@ suite('NotebookExecutionStateService', () => { }); }); -class TestNotebookKernel implements IResolvedNotebookKernel { - type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; +class TestNotebookKernel implements INotebookKernel { id: string = 'test'; label: string = ''; viewType = '*'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts index 62d6afecb8b..1b03a84c623 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { mock } from 'vs/base/test/common/mock'; @@ -16,6 +16,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { DisposableStore } from 'vs/base/common/lifecycle'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; suite('NotebookKernelService', () => { @@ -37,6 +38,15 @@ suite('NotebookKernelService', () => { override onWillRemoveNotebookDocument = Event.None; override getNotebookTextModels() { return []; } }); + instantiationService.stub(IMenuService, new class extends mock<IMenuService>() { + override createMenu() { + return new class extends mock<IMenu>() { + override onDidChange = Event.None; + override getActions() { return []; } + override dispose() { } + }; + } + }); kernelService = instantiationService.createInstance(NotebookKernelService); instantiationService.set(INotebookKernelService, kernelService); }); @@ -159,8 +169,7 @@ suite('NotebookKernelService', () => { }); }); -class TestNotebookKernel implements IResolvedNotebookKernel { - type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; +class TestNotebookKernel implements INotebookKernel { id: string = Math.random() + 'kernel'; label: string = 'test-label'; viewType = '*'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 514c5231443..8570bf9146a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -422,7 +422,7 @@ class TestNotebookExecutionStateService implements INotebookExecutionStateServic return this._executions.get(cellUri); } - createCellExecution(controllerId: string, notebook: URI, cellHandle: number): INotebookCellExecution { + createCellExecution(notebook: URI, cellHandle: number): INotebookCellExecution { const onComplete = () => this._executions.delete(CellUri.generate(notebook, cellHandle)); const exe = new TestCellExecution(notebook, cellHandle, onComplete); this._executions.set(CellUri.generate(notebook, cellHandle), exe); diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 8a345e3f73b..8b8a7ab8180 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -136,7 +136,7 @@ export class OutlinePane extends ViewPane { this._domNode = container; container.classList.add('outline-pane'); - let progressContainer = dom.$('.outline-progress'); + const progressContainer = dom.$('.outline-progress'); this._message = dom.$('.outline-message'); this._progressBar = new ProgressBar(progressContainer); diff --git a/src/vs/workbench/contrib/outline/browser/outlineViewState.ts b/src/vs/workbench/contrib/outline/browser/outlineViewState.ts index 1d970d90567..a5c2bb4d093 100644 --- a/src/vs/workbench/contrib/outline/browser/outlineViewState.ts +++ b/src/vs/workbench/contrib/outline/browser/outlineViewState.ts @@ -68,7 +68,7 @@ export class OutlineViewState { } restore(storageService: IStorageService): void { - let raw = storageService.get('outline/state', StorageScope.WORKSPACE); + const raw = storageService.get('outline/state', StorageScope.WORKSPACE); if (!raw) { return; } diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index 3c78aa114d1..af5562f11ce 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -64,9 +64,10 @@ export class LogViewer extends AbstractTextResourceEditor { @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @IFileService fileService: IFileService ) { - super(LogViewer.LOG_VIEWER_EDITOR_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService); + super(LogViewer.LOG_VIEWER_EDITOR_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); } protected override getConfigurationOverrides(): IEditorOptions { diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index dffb7046911..c13ec6b5e0c 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -145,9 +145,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo this.setActiveChannel(channel); this._onActiveOutputChannel.fire(channelId); const outputView = this.viewsService.getActiveViewWithId<OutputViewPane>(OUTPUT_VIEW_ID); - if (outputView) { - outputView.showChannel(channel, true); - } + outputView?.showChannel(channel, true); } } @@ -204,7 +202,7 @@ export class LogContentProvider { provideTextContent(resource: URI): Promise<ITextModel> | null { if (resource.scheme === LOG_SCHEME) { - let channelModel = this.getChannelModel(resource); + const channelModel = this.getChannelModel(resource); if (channelModel) { return channelModel.loadModel(); } diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 545d41fdcee..a3b51602a06 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -38,6 +38,7 @@ import { Dimension } from 'vs/base/browser/dom'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { IFileService } from 'vs/platform/files/common/files'; export class OutputViewPane extends ViewPane { @@ -83,9 +84,7 @@ export class OutputViewPane extends ViewPane { override focus(): void { super.focus(); - if (this.editorPromise) { - this.editorPromise.then(() => this.editor.focus()); - } + this.editorPromise?.then(() => this.editor.focus()); } override renderBody(container: HTMLElement): void { @@ -182,9 +181,10 @@ export class OutputEditor extends AbstractTextResourceEditor { @IThemeService themeService: IThemeService, @IOutputService private readonly outputService: IOutputService, @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IEditorService editorService: IEditorService + @IEditorService editorService: IEditorService, + @IFileService fileService: IFileService ) { - super(OUTPUT_VIEW_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService); + super(OUTPUT_VIEW_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); } override getId(): string { @@ -288,7 +288,7 @@ class SwitchOutputActionViewItem extends SelectActionViewItem { ) { super(null, action, [], 0, contextViewService, { ariaLabel: nls.localize('outputChannels', "Output Channels"), optionsAsChildren: true }); - let outputChannelRegistry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels); + const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels); this._register(outputChannelRegistry.onDidRegisterChannel(() => this.updateOptions())); this._register(outputChannelRegistry.onDidRemoveChannel(() => this.updateOptions())); this._register(this.outputService.onActiveOutputChannel(() => this.updateOptions())); diff --git a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts index 35450e8bf61..594c21561eb 100644 --- a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts +++ b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts @@ -23,9 +23,9 @@ suite('OutputLinkProvider', () => { const rootFolder = isWindows ? URI.file('C:\\Users\\someone\\AppData\\Local\\Temp\\_monacodata_9888\\workspaces\\mankala') : URI.file('C:/Users/someone/AppData/Local/Temp/_monacodata_9888/workspaces/mankala'); - let patterns = OutputLinkComputer.createPatterns(rootFolder); + const patterns = OutputLinkComputer.createPatterns(rootFolder); - let contextService = new TestContextService(); + const contextService = new TestContextService(); let line = toOSPath('Foo bar'); let result = OutputLinkComputer.detectLinks(line, 1, patterns, contextService); @@ -285,12 +285,12 @@ suite('OutputLinkProvider', () => { const rootFolder = isWindows ? URI.file('C:\\Users\\username\\Desktop\\test-ts') : URI.file('C:/Users/username/Desktop'); - let patterns = OutputLinkComputer.createPatterns(rootFolder); + const patterns = OutputLinkComputer.createPatterns(rootFolder); - let contextService = new TestContextService(); + const contextService = new TestContextService(); - let line = toOSPath('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa C:\\Users\\username\\Desktop\\test-ts\\prj.conf C:\\Users\\username\\Desktop\\test-ts\\prj.conf C:\\Users\\username\\Desktop\\test-ts\\prj.conf'); - let result = OutputLinkComputer.detectLinks(line, 1, patterns, contextService); + const line = toOSPath('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa C:\\Users\\username\\Desktop\\test-ts\\prj.conf C:\\Users\\username\\Desktop\\test-ts\\prj.conf C:\\Users\\username\\Desktop\\test-ts\\prj.conf'); + const result = OutputLinkComputer.detectLinks(line, 1, patterns, contextService); assert.strictEqual(result.length, 3); for (const res of result) { diff --git a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index ed9f1f7133f..3d1ddf27618 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -95,9 +95,7 @@ class PerfModelContentProvider implements ITextModelContentProvider { this._model = this._modelService.getModel(resource) || this._modelService.createModel('Loading...', langId, resource); this._modelDisposables.push(langId.onDidChange(e => { - if (this._model) { - this._model.setMode(e); - } + this._model?.setMode(e); })); this._modelDisposables.push(this._extensionService.onDidChangeExtensionsStatus(this._updateModel, this)); @@ -116,8 +114,8 @@ class PerfModelContentProvider implements ITextModelContentProvider { ]).then(() => { if (this._model && !this._model.isDisposed()) { - let stats = LoaderStats.get(); - let md = new MarkdownBuilder(); + const stats = LoaderStats.get(); + const md = new MarkdownBuilder(); this._addSummary(md); md.blank(); this._addSummaryTable(md, stats); @@ -195,8 +193,8 @@ class PerfModelContentProvider implements ITextModelContentProvider { const eager: ({ toString(): string })[][] = []; const normal: ({ toString(): string })[][] = []; - let extensionsStatus = this._extensionService.getExtensionsStatus(); - for (let id in extensionsStatus) { + const extensionsStatus = this._extensionService.getExtensionsStatus(); + for (const id in extensionsStatus) { const { activationTimes: times } = extensionsStatus[id]; if (!times) { continue; @@ -220,14 +218,14 @@ class PerfModelContentProvider implements ITextModelContentProvider { private _addRawPerfMarks(md: MarkdownBuilder): void { - for (let [source, marks] of this._timerService.getPerformanceMarks()) { + for (const [source, marks] of this._timerService.getPerformanceMarks()) { md.heading(2, `Raw Perf Marks: ${source}`); md.value += '```\n'; md.value += `Name\tTimestamp\tDelta\tTotal\n`; let lastStartTime = -1; let total = 0; for (const { name, startTime } of marks) { - let delta = lastStartTime !== -1 ? startTime - lastStartTime : 0; + const delta = lastStartTime !== -1 ? startTime - lastStartTime : 0; total += delta; md.value += `${name}\t${startTime}\t${delta}\t${total}\n`; lastStartTime = startTime; diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 018b31c5768..c6312374488 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -50,11 +50,6 @@ import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browse import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -type KeybindingEditorActionClassification = { - action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - command: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; -}; - const $ = DOM.$; class ThemableToggleActionViewItem extends ToggleActionViewItem { @@ -188,7 +183,6 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP try { const key = await this.defineKeybindingWidget.define(); if (key) { - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_DEFINE, keybindingEntry.keybindingItem.command); await this.updateKeybinding(keybindingEntry, key, keybindingEntry.keybindingItem.when, add); } } catch (error) { @@ -223,7 +217,6 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP async removeKeybinding(keybindingEntry: IKeybindingItemEntry): Promise<void> { this.selectEntry(keybindingEntry); if (keybindingEntry.keybindingItem.keybinding) { // This should be a pre-condition - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_REMOVE, keybindingEntry.keybindingItem.command); try { await this.keybindingEditingService.removeKeybinding(keybindingEntry.keybindingItem.keybindingItem); this.focus(); @@ -236,7 +229,6 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP async resetKeybinding(keybindingEntry: IKeybindingItemEntry): Promise<void> { this.selectEntry(keybindingEntry); - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_RESET, keybindingEntry.keybindingItem.command); try { await this.keybindingEditingService.resetKeybinding(keybindingEntry.keybindingItem.keybindingItem); if (!keybindingEntry.keybindingItem.keybinding) { // reveal only if keybinding was added to unassinged. Because the entry will be placed in different position after rendering @@ -251,7 +243,6 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP async copyKeybinding(keybinding: IKeybindingItemEntry): Promise<void> { this.selectEntry(keybinding); - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY, keybinding.keybindingItem.command); const userFriendlyKeybinding: IUserFriendlyKeybinding = { key: keybinding.keybindingItem.keybinding ? keybinding.keybindingItem.keybinding.getUserSettingsLabel() || '' : '', command: keybinding.keybindingItem.command @@ -264,13 +255,11 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP async copyKeybindingCommand(keybinding: IKeybindingItemEntry): Promise<void> { this.selectEntry(keybinding); - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, keybinding.keybindingItem.command); await this.clipboardService.writeText(keybinding.keybindingItem.command); } async copyKeybindingCommandTitle(keybinding: IKeybindingItemEntry): Promise<void> { this.selectEntry(keybinding); - this.reportKeybindingAction(KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, keybinding.keybindingItem.command); await this.clipboardService.writeText(keybinding.keybindingItem.commandLabel); } @@ -304,7 +293,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP private createOverlayContainer(parent: HTMLElement): void { this.overlayContainer = DOM.append(parent, $('.overlay-container')); this.overlayContainer.style.position = 'absolute'; - this.overlayContainer.style.zIndex = '10'; + this.overlayContainer.style.zIndex = '40'; // has to greater than sash z-index which is 35 this.defineKeybindingWidget = this._register(this.instantiationService.createInstance(DefineKeybindingWidget, this.overlayContainer)); this._register(this.defineKeybindingWidget.onDidChange(keybindingStr => this.defineKeybindingWidget.printExisting(this.keybindingsEditorModel!.fetch(`"${keybindingStr}"`).length))); this._register(this.defineKeybindingWidget.onShowExistingKeybidings(keybindingStr => this.searchWidget.setValue(`"${keybindingStr}"`))); @@ -334,7 +323,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP ariaLabelledBy: 'keybindings-editor-aria-label-element', recordEnter: true, quoteRecordedKeys: true, - history: this.getMemento(StorageScope.GLOBAL, StorageTarget.USER)['searchHistory'] || [], + history: this.getMemento(StorageScope.PROFILE, StorageTarget.USER)['searchHistory'] || [], })); this._register(this.searchWidget.onDidChange(searchValue => { clearInputAction.enabled = !!searchValue; @@ -382,7 +371,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP getKeyBinding: action => this.keybindingsService.lookupKeybinding(action.id) })); toolBar.setActions(actions); - this._register(this.keybindingsService.onDidUpdateKeybindings(e => toolBar.setActions(actions))); + this._register(this.keybindingsService.onDidUpdateKeybindings(() => toolBar.setActions(actions))); } private updateSearchOptions(): void { @@ -501,6 +490,10 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP this.keybindingFocusContextKey.reset(); })); this._register(this.keybindingsTable.onDidOpen((e) => { + // stop double click action on the input #148493 + if (e.browserEvent?.defaultPrevented) { + return; + } const activeKeybindingEntry = this.activeKeybindingEntry; if (activeKeybindingEntry) { this.defineKeybinding(activeKeybindingEntry, false); @@ -541,11 +534,17 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP this.renderKeybindingsEntries(this.searchWidget.hasFocus()); this.searchHistoryDelayer.trigger(() => { this.searchWidget.inputBox.addToHistory(); - this.getMemento(StorageScope.GLOBAL, StorageTarget.USER)['searchHistory'] = this.searchWidget.inputBox.getHistory(); + this.getMemento(StorageScope.PROFILE, StorageTarget.USER)['searchHistory'] = this.searchWidget.inputBox.getHistory(); this.saveState(); }); } + public clearKeyboardShortcutSearchHistory(): void { + this.searchWidget.inputBox.clearHistory(); + this.getMemento(StorageScope.PROFILE, StorageTarget.USER)['searchHistory'] = this.searchWidget.inputBox.getHistory(); + this.saveState(); + } + private renderKeybindingsEntries(reset: boolean, preserveFocus?: boolean): void { if (this.keybindingsEditorModel) { const filter = this.searchWidget.getValue(); @@ -774,10 +773,6 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP }; } - private reportKeybindingAction(action: string, command: string): void { - this.telemetryService.publicLog2<{ action: string; command: string }, KeybindingEditorActionClassification>('keybindingsEditor.action', { command, action }); - } - private onKeybindingEditingError(error: any): void { this.notificationService.error(typeof error === 'string' ? error : localize('error', "Error '{0}' while editing the keybinding. Please open 'keybindings.json' file and check for errors.", `${error}`)); } @@ -1072,6 +1067,9 @@ class WhenColumnRenderer implements ITableRenderer<IKeybindingItemEntry, IWhenCo _onDidReject.fire(); }))); + // stop double click action on the input #148493 + disposables.add((DOM.addDisposableListener(whenInput.inputElement, DOM.EventType.DBLCLICK, e => DOM.EventHelper.stop(e)))); + const renderDisposables = disposables.add(new DisposableStore()); return { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts index e69f1504400..fd43f3b24f5 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts @@ -31,8 +31,8 @@ import { KeybindingParser } from 'vs/base/common/keybindingParser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { equals } from 'vs/base/common/arrays'; import { assertIsDefined } from 'vs/base/common/types'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isEqual } from 'vs/base/common/resources'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; const NLS_LAUNCH_MESSAGE = nls.localize('defineKeybinding.start', "Define Keybinding"); const NLS_KB_LAYOUT_ERROR_MESSAGE = nls.localize('defineKeybinding.kbLayoutErrorMessage', "You won't be able to produce this key combination under your current keyboard layout."); @@ -51,7 +51,7 @@ export class DefineKeybindingController extends Disposable implements IEditorCon constructor( private _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService + @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService ) { super(); @@ -70,7 +70,7 @@ export class DefineKeybindingController extends Disposable implements IEditorCon } private _update(): void { - if (!isInterestingEditorModel(this._editor, this._environmentService)) { + if (!isInterestingEditorModel(this._editor, this._userDataProfileService)) { this._disposeKeybindingWidgetRenderer(); this._disposeKeybindingDecorationRenderer(); return; @@ -175,7 +175,7 @@ export class KeybindingEditorDecorationsRenderer extends Disposable { const model = assertIsDefined(this._editor.getModel()); this._register(model.onDidChangeContent(() => this._updateDecorations.schedule())); - this._register(this._keybindingService.onDidUpdateKeybindings((e) => this._updateDecorations.schedule())); + this._register(this._keybindingService.onDidUpdateKeybindings(() => this._updateDecorations.schedule())); this._register({ dispose: () => { this._dec.clear(); @@ -365,7 +365,7 @@ class DefineKeybindingCommand extends EditorCommand { } runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void { - if (!isInterestingEditorModel(editor, accessor.get(IEnvironmentService)) || editor.getOption(EditorOption.readOnly)) { + if (!isInterestingEditorModel(editor, accessor.get(IUserDataProfileService)) || editor.getOption(EditorOption.readOnly)) { return; } const controller = DefineKeybindingController.get(editor); @@ -375,12 +375,12 @@ class DefineKeybindingCommand extends EditorCommand { } } -function isInterestingEditorModel(editor: ICodeEditor, environmentService: IEnvironmentService): boolean { +function isInterestingEditorModel(editor: ICodeEditor, userDataProfileService: IUserDataProfileService): boolean { const model = editor.getModel(); if (!model) { return false; } - return isEqual(model.uri, environmentService.keybindingsResource); + return isEqual(model.uri, userDataProfileService.currentProfile.keybindingsResource); } registerEditorContribution(DefineKeybindingController.ID, DefineKeybindingController); diff --git a/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts b/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts index 08d6550d21c..f5dff19e6d8 100644 --- a/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts +++ b/src/vs/workbench/contrib/preferences/browser/keyboardLayoutPicker.ts @@ -34,9 +34,9 @@ export class KeyboardLayoutPickerContribution extends Disposable implements IWor const name = nls.localize('status.workbench.keyboardLayout', "Keyboard Layout"); - let layout = this.keyboardLayoutService.getCurrentKeyboardLayout(); + const layout = this.keyboardLayoutService.getCurrentKeyboardLayout(); if (layout) { - let layoutInfo = parseKeyboardLayoutDescription(layout); + const layoutInfo = parseKeyboardLayoutDescription(layout); const text = nls.localize('keyboardLayout', "Layout: {0}", layoutInfo.label); this.pickerElement.value = this.statusbarService.addEntry( @@ -52,8 +52,8 @@ export class KeyboardLayoutPickerContribution extends Disposable implements IWor } this._register(this.keyboardLayoutService.onDidChangeKeyboardLayout(() => { - let layout = this.keyboardLayoutService.getCurrentKeyboardLayout(); - let layoutInfo = parseKeyboardLayoutDescription(layout); + const layout = this.keyboardLayoutService.getCurrentKeyboardLayout(); + const layoutInfo = parseKeyboardLayoutDescription(layout); if (this.pickerElement.value) { const text = nls.localize('keyboardLayout', "Layout: {0}", layoutInfo.label); @@ -119,10 +119,10 @@ export class KeyboardLayoutPickerAction extends Action { } override async run(): Promise<void> { - let layouts = this.keyboardLayoutService.getAllKeyboardLayouts(); - let currentLayout = this.keyboardLayoutService.getCurrentKeyboardLayout(); - let layoutConfig = this.configurationService.getValue('keyboard.layout'); - let isAutoDetect = layoutConfig === 'autodetect'; + const layouts = this.keyboardLayoutService.getAllKeyboardLayouts(); + const currentLayout = this.keyboardLayoutService.getCurrentKeyboardLayout(); + const layoutConfig = this.configurationService.getValue('keyboard.layout'); + const isAutoDetect = layoutConfig === 'autodetect'; const picks: QuickPickInput[] = layouts.map(layout => { const picked = !isAutoDetect && areKeyboardLayoutsEqual(currentLayout, layout); @@ -143,7 +143,7 @@ export class KeyboardLayoutPickerAction extends Action { picks.unshift({ type: 'separator', label: nls.localize('layoutPicks', "Keyboard Layouts ({0})", platform) }); } - let configureKeyboardLayout: IQuickPickItem = { label: nls.localize('configureKeyboardLayout', "Configure Keyboard Layout") }; + const configureKeyboardLayout: IQuickPickItem = { label: nls.localize('configureKeyboardLayout', "Configure Keyboard Layout") }; picks.unshift(configureKeyboardLayout); diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 92733560d7b..d44eec7e5d5 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -109,15 +109,23 @@ /* padding must be on action-label because it has the bottom-border, because that's where the .checked class is */ } +.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget > .monaco-action-bar .action-item.focused { + outline-offset: -1.5px; +} + .settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget > .monaco-action-bar .action-item .action-label { text-transform: none; font-size: 13px; - padding-bottom: 7px; + padding-bottom: 6.5px; padding-top: 7px; padding-left: 8px; padding-right: 8px; } +.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget > .monaco-action-bar .action-item .action-label .dropdown-icon { + padding-top: 2px; +} + .settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget > .monaco-action-bar .action-item .action-label:not(.checked):not(:focus) { /* Add an extra pixel due to it not getting the outline */ padding-bottom: 8px; @@ -343,14 +351,19 @@ font-style: italic; } -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .setting-item-ignored .codicon, -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .setting-item-default-overridden .codicon { - vertical-align: text-top; +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title > .misc-label .setting-item-overrides:hover, +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title > .misc-label .setting-item-ignored:hover, +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title > .misc-label .setting-item-default-overridden:hover { + text-decoration: underline; +} + +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .codicon { + vertical-align: middle; padding-left: 1px; } .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .setting-item-label .codicon { - vertical-align: text-top; + vertical-align: middle; } .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .setting-item-overrides a.modified-scope { @@ -399,13 +412,19 @@ -webkit-user-select: text; } -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-untrusted > .setting-item-contents .setting-item-trust-description { +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-untrusted > .setting-item-contents .setting-item-trust-description, +.settings-editor > .settings-body .settings-tree-container .setting-item > .setting-item-contents .setting-item-policy-description { display: flex; font-weight: 600; margin: 6px 0 12px 0; } -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-untrusted > .setting-item-contents .setting-item-trust-description > span { +.settings-editor > .settings-body .settings-tree-container .setting-item > .setting-item-contents .setting-item-policy-description[hidden] { + display: none; +} + +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-untrusted > .setting-item-contents .setting-item-trust-description > span, +.settings-editor > .settings-body .settings-tree-container .setting-item > .setting-item-contents .setting-item-policy-description > span { padding-right: 5px; } @@ -413,6 +432,10 @@ color: var(--workspace-trust-state-untrusted-color) !important; } +.settings-editor > .settings-body .settings-tree-container .setting-item > .setting-item-contents .setting-item-policy-description > span.codicon.codicon-lock { + color: var(--organization-policy-icon-color) !important; +} + .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-validation-message { display: none; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index b5230a5af22..f401ea19b4d 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -34,7 +34,7 @@ import { ConfigureLanguageBasedSettingsAction } from 'vs/workbench/contrib/prefe import { SettingsEditorContribution } from 'vs/workbench/contrib/preferences/browser/preferencesEditor'; import { preferencesOpenSettingsIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { SettingsEditor2, SettingsFocusContext } from 'vs/workbench/contrib/preferences/browser/settingsEditor2'; -import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_HISTORY, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { PreferencesContribution } from 'vs/workbench/contrib/preferences/common/preferencesContribution'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -43,6 +43,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browser/keybindingsEditorInput'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -139,6 +140,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon constructor( @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ILabelService private readonly labelService: ILabelService, @@ -242,25 +244,32 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon return accessor.get(IPreferencesService).openRawDefaultSettings(); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: '_workbench.openUserSettingsEditor', - title: OPEN_SETTINGS2_ACTION_TITLE, - icon: preferencesOpenSettingsIcon, - menu: [{ - id: MenuId.EditorTitle, - when: ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(that.environmentService.settingsResource.toString()), ContextKeyExpr.not('isInDiffEditor')), - group: 'navigation', - order: 1 - }] - }); - } - run(accessor: ServicesAccessor, args: IOpenSettingsActionOptions) { - args = sanitizeOpenSettingsArgs(args); - return accessor.get(IPreferencesService).openUserSettings({ jsonEditor: false, ...args }); - } - }); + + const registerOpenUserSettingsEditorFromJsonActionDisposable = this._register(new MutableDisposable()); + const registerOpenUserSettingsEditorFromJsonAction = () => { + registerOpenUserSettingsEditorFromJsonActionDisposable.value = registerAction2(class extends Action2 { + constructor() { + super({ + id: '_workbench.openUserSettingsEditor', + title: OPEN_SETTINGS2_ACTION_TITLE, + icon: preferencesOpenSettingsIcon, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(that.userDataProfileService.currentProfile.settingsResource.toString()), ContextKeyExpr.not('isInDiffEditor')), + group: 'navigation', + order: 1 + }] + }); + } + run(accessor: ServicesAccessor, args: IOpenSettingsActionOptions) { + args = sanitizeOpenSettingsArgs(args); + return accessor.get(IPreferencesService).openUserSettings({ jsonEditor: false, ...args }); + } + }); + }; + registerOpenUserSettingsEditorFromJsonAction(); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => registerOpenUserSettingsEditorFromJsonAction())); + registerAction2(class extends Action2 { constructor() { super({ @@ -726,34 +735,39 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon private registerKeybindingsActions() { const that = this; const category = { value: nls.localize('preferences', "Preferences"), original: 'Preferences' }; - registerAction2(class extends Action2 { - constructor() { - super({ - id: 'workbench.action.openGlobalKeybindings', - title: { value: nls.localize('openGlobalKeybindings', "Open Keyboard Shortcuts"), original: 'Open Keyboard Shortcuts' }, - category, - icon: preferencesOpenSettingsIcon, - keybinding: { - when: null, - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyS) - }, - menu: [ - { id: MenuId.CommandPalette }, - { - id: MenuId.EditorTitle, - when: ResourceContextKey.Resource.isEqualTo(that.environmentService.keybindingsResource.toString()), - group: 'navigation', - order: 1, - } - ] - }); - } - run(accessor: ServicesAccessor, args: string | undefined) { - const query = typeof args === 'string' ? args : undefined; - return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query }); - } - }); + const registerOpenGlobalKeybindingsActionDisposable = this._register(new MutableDisposable()); + const registerOpenGlobalKeybindingsAction = () => { + registerOpenGlobalKeybindingsActionDisposable.value = registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.openGlobalKeybindings', + title: { value: nls.localize('openGlobalKeybindings', "Open Keyboard Shortcuts"), original: 'Open Keyboard Shortcuts' }, + category, + icon: preferencesOpenSettingsIcon, + keybinding: { + when: null, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyS) + }, + menu: [ + { id: MenuId.CommandPalette }, + { + id: MenuId.EditorTitle, + when: ResourceContextKey.Resource.isEqualTo(that.userDataProfileService.currentProfile.keybindingsResource.toString()), + group: 'navigation', + order: 1, + } + ] + }); + } + run(accessor: ServicesAccessor, args: string | undefined) { + const query = typeof args === 'string' ? args : undefined; + return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query }); + } + }); + }; + registerOpenGlobalKeybindingsAction(); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => registerOpenGlobalKeybindingsAction())); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { command: { id: 'workbench.action.openGlobalKeybindings', @@ -887,6 +901,28 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_HISTORY, + title: nls.localize('clearHistory', "Clear Keyboard Shortcuts Search History"), + category, + menu: [ + { + id: MenuId.CommandPalette, + when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR), + } + ] + }); + } + run(accessor: ServicesAccessor) { + const editorPane = accessor.get(IEditorService).activeEditorPane; + if (editorPane instanceof KeybindingsEditor) { + editorPane.clearKeyboardShortcutSearchHistory(); + } + } + }); + this.registerKeybindingEditorActions(); } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 3bb3a346bd3..a2aa81b94c1 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -175,6 +175,7 @@ class EditSettingRenderer extends Disposable { constructor(private editor: ICodeEditor, private primarySettingsModel: ISettingsEditorModel, private settingHighlighter: SettingHighlighter, + @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { @@ -283,6 +284,9 @@ class EditSettingRenderer extends Disposable { return this.getSettingsAtLineNumber(lineNumber).filter(setting => { const configurationNode = configurationMap[setting.key]; if (configurationNode) { + if (configurationNode.policy && this.configurationService.inspect(setting.key).policyValue !== undefined) { + return false; + } if (this.isDefaultSettings()) { if (setting.key === 'launch') { // Do not show because of https://github.com/microsoft/vscode/issues/32593 @@ -522,6 +526,9 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc for (const setting of section.settings) { const configuration = configurationRegistry[setting.key]; if (configuration) { + if (this.handlePolicyConfiguration(setting, configuration, markerData)) { + continue; + } switch (this.settingsEditorModel.configurationTarget) { case ConfigurationTarget.USER_LOCAL: this.handleLocalUserConfiguration(setting, configuration, markerData); @@ -550,6 +557,25 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc return markerData; } + private handlePolicyConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): boolean { + if (!configuration.policy) { + return false; + } + if (this.configurationService.inspect(setting.key).policyValue === undefined) { + return false; + } + if (this.settingsEditorModel.configurationTarget === ConfigurationTarget.DEFAULT) { + return false; + } + markerData.push({ + severity: MarkerSeverity.Hint, + tags: [MarkerTag.Unnecessary], + ...setting.range, + message: nls.localize('unsupportedPolicySetting', "This setting cannot be applied because it is configured in the system policy.") + }); + return true; + } + private handleLocalUserConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): void { if (this.environmentService.remoteAuthority && (configuration.scope === ConfigurationScope.MACHINE || configuration.scope === ConfigurationScope.MACHINE_OVERRIDABLE)) { markerData.push({ diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index f74ec72414c..cab53e0d6a6 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -463,15 +463,11 @@ export class SearchWidget extends Widget { layout(dimension: DOM.Dimension) { if (dimension.width < 400) { - if (this.countElement) { - this.countElement.classList.add('hide'); - } + this.countElement?.classList.add('hide'); this.inputBox.inputElement.style.paddingRight = '0px'; } else { - if (this.countElement) { - this.countElement.classList.remove('hide'); - } + this.countElement?.classList.remove('hide'); this.inputBox.inputElement.style.paddingRight = this.getControlsWidth() + 'px'; } @@ -506,9 +502,7 @@ export class SearchWidget extends Widget { } override dispose(): void { - if (this.options.focusKey) { - this.options.focusKey.set(false); - } + this.options.focusKey?.set(false); super.dispose(); } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index d2d2c183503..83d1addd26d 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -40,10 +40,10 @@ import { IEditorMemento, IEditorOpenContext, IEditorPane } from 'vs/workbench/co import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; +import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; @@ -60,6 +60,7 @@ import { Color } from 'vs/base/common/color'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { SettingsSearchFilterDropdownMenuActionViewItem } from 'vs/workbench/contrib/preferences/browser/settingsSearchMenu'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ISettingOverrideClickEvent } from 'vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators'; export const enum SettingsFocusContext { Search, @@ -126,6 +127,7 @@ export class SettingsEditor2 extends EditorPane { `@${FEATURE_SETTING_TAG}remote`, `@${FEATURE_SETTING_TAG}timeline`, `@${FEATURE_SETTING_TAG}notebook`, + `@${POLICY_SETTING_TAG}` ]; private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean { @@ -252,9 +254,7 @@ export class SettingsEditor2 extends EditorPane { })); this._register(workspaceTrustManagementService.onDidChangeTrust(() => { - if (this.searchResultModel) { - this.searchResultModel.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()); - } + this.searchResultModel?.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()); if (this.settingsTreeModel) { this.settingsTreeModel.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()); @@ -508,7 +508,7 @@ export class SettingsEditor2 extends EditorPane { } clearSearchFilters(): void { - let query = this.searchWidget.getValue(); + const query = this.searchWidget.getValue(); const splitQuery = query.split(' ').filter(word => { return word.length && !SettingsEditor2.SUGGESTIONS.some(suggestion => word.startsWith(suggestion)); @@ -740,7 +740,7 @@ export class SettingsEditor2 extends EditorPane { orientation: Orientation.HORIZONTAL, proportionalLayout: true }); - const startingWidth = this.storageService.getNumber('settingsEditor2.splitViewWidth', StorageScope.GLOBAL, SettingsEditor2.TOC_RESET_WIDTH); + const startingWidth = this.storageService.getNumber('settingsEditor2.splitViewWidth', StorageScope.PROFILE, SettingsEditor2.TOC_RESET_WIDTH); this.splitView.addView({ onDidChange: Event.None, element: this.tocTreeContainer, @@ -768,7 +768,7 @@ export class SettingsEditor2 extends EditorPane { })); this._register(this.splitView.onDidSashChange(() => { const width = this.splitView.getViewSize(0); - this.storageService.store('settingsEditor2.splitViewWidth', width, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store('settingsEditor2.splitViewWidth', width, StorageScope.PROFILE, StorageTarget.USER); })); const borderColor = this.theme.getColor(settingsSashBorder)!; this.splitView.style({ separatorBorder: borderColor }); @@ -834,8 +834,23 @@ export class SettingsEditor2 extends EditorPane { })); } - private createSettingsTree(container: HTMLElement): void { + private applyFilter(filter: string) { + if (this.searchWidget && !this.searchWidget.getValue().includes(filter)) { + // Prepend the filter to the query. + const newQuery = `${filter} ${this.searchWidget.getValue().trimStart()}`; + this.focusSearch(newQuery, false); + } + } + private removeLanguageFilters() { + if (this.searchWidget && this.searchWidget.getValue().includes(`@${LANGUAGE_SETTING_TAG}`)) { + const query = this.searchWidget.getValue().split(' '); + const newQuery = query.filter(word => !word.startsWith(`@${LANGUAGE_SETTING_TAG}`)).join(' '); + this.focusSearch(newQuery, false); + } + } + + private createSettingsTree(container: HTMLElement): void { this.settingRenderers = this.instantiationService.createInstance(SettingTreeRenderers); this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset))); this._register(this.settingRenderers.onDidOpenSettings(settingKey => { @@ -847,17 +862,6 @@ export class SettingsEditor2 extends EditorPane { this._currentFocusContext = SettingsFocusContext.SettingControl; this.settingRowFocused.set(false); })); - this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => { - if (element.scope.toLowerCase() === 'workspace') { - this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE); - } else if (element.scope.toLowerCase() === 'user') { - this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_LOCAL); - } else if (element.scope.toLowerCase() === 'remote') { - this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_REMOTE); - } - - this.searchWidget.setValue(element.targetKey); - })); this._register(this.settingRenderers.onDidChangeSettingHeight((params: HeightChangeParams) => { const { element, height } = params; try { @@ -866,12 +870,21 @@ export class SettingsEditor2 extends EditorPane { // the element was not found } })); - this._register(this.settingRenderers.onApplyLanguageFilter((lang: string) => { - if (this.searchWidget) { - // Prepend the language filter to the query. - const newQuery = `@${LANGUAGE_SETTING_TAG}${lang} ${this.searchWidget.getValue().trimStart()}`; - this.focusSearch(newQuery, false); + this._register(this.settingRenderers.onApplyFilter((filter) => this.applyFilter(filter))); + this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => { + this.removeLanguageFilters(); + if (element.language) { + this.applyFilter(`@${LANGUAGE_SETTING_TAG}${element.language}`); + } + + if (element.scope === 'workspace') { + this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE); + } else if (element.scope === 'user') { + this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_LOCAL); + } else if (element.scope === 'remote') { + this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_REMOTE); } + this.applyFilter(`@${ID_SETTING_TAG}${element.settingKey}`); })); this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree, @@ -894,7 +907,8 @@ export class SettingsEditor2 extends EditorPane { })); this._register(this.settingsTree.onDidFocus(() => { - if (document.activeElement?.classList.contains('monaco-list')) { + const classList = document.activeElement?.classList; + if (classList && classList.contains('monaco-list') && classList.contains('settings-editor-tree')) { this._currentFocusContext = SettingsFocusContext.SettingTree; this.settingRowFocused.set(true); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts new file mode 100644 index 00000000000..a10179c4ac7 --- /dev/null +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -0,0 +1,314 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { ICustomHover, ITooltipMarkdownString, IUpdatableHoverOptions, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; +import { Emitter } from 'vs/base/common/event'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; +import { getDefaultIgnoredSettings, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; +import { MODIFIED_INDICATOR_USE_INLINE_ONLY } from 'vs/workbench/contrib/preferences/common/preferences'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; + +const $ = DOM.$; + +type ScopeString = 'workspace' | 'user' | 'remote'; + +export interface ISettingOverrideClickEvent { + scope: ScopeString; + language: string; + settingKey: string; +} + +/** + * Renders the indicators next to a setting, such as "Also Modified In". + */ +export class SettingsTreeIndicatorsLabel implements IDisposable { + private indicatorsContainerElement: HTMLElement; + private scopeOverridesElement: HTMLElement; + private scopeOverridesLabel: SimpleIconLabel; + private syncIgnoredElement: HTMLElement; + private syncIgnoredHover: ICustomHover | undefined; + private defaultOverrideIndicatorElement: HTMLElement; + private hoverDelegate: IHoverDelegate; + private hover: ICustomHover | undefined; + + constructor( + container: HTMLElement, + @IConfigurationService configurationService: IConfigurationService, + @IHoverService hoverService: IHoverService, + @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, + @ILanguageService private readonly languageService: ILanguageService) { + this.indicatorsContainerElement = DOM.append(container, $('.misc-label')); + this.indicatorsContainerElement.style.display = 'inline'; + + const scopeOverridesIndicator = this.createScopeOverridesIndicator(); + this.scopeOverridesElement = scopeOverridesIndicator.element; + this.scopeOverridesLabel = scopeOverridesIndicator.label; + this.syncIgnoredElement = this.createSyncIgnoredElement(); + this.defaultOverrideIndicatorElement = this.createDefaultOverrideIndicator(); + + this.hoverDelegate = { + showHover: (options: IHoverDelegateOptions, focus?: boolean) => { + return hoverService.showHover(options, focus); + }, + onDidHideHover: () => { }, + delay: configurationService.getValue<number>('workbench.hover.delay'), + placement: 'element' + }; + } + + private createScopeOverridesIndicator(): { element: HTMLElement; label: SimpleIconLabel } { + const otherOverridesElement = $('span.setting-item-overrides'); + const otherOverridesLabel = new SimpleIconLabel(otherOverridesElement); + return { element: otherOverridesElement, label: otherOverridesLabel }; + } + + private createSyncIgnoredElement(): HTMLElement { + const syncIgnoredElement = $('span.setting-item-ignored'); + const syncIgnoredLabel = new SimpleIconLabel(syncIgnoredElement); + syncIgnoredLabel.text = localize('extensionSyncIgnoredLabel', 'Not synced'); + const syncIgnoredHoverContent = localize('syncIgnoredTitle', "This setting is ignored during sync"); + this.syncIgnoredHover = setupCustomHover(this.hoverDelegate, syncIgnoredElement, syncIgnoredHoverContent); + return syncIgnoredElement; + } + + private createDefaultOverrideIndicator(): HTMLElement { + const defaultOverrideIndicator = $('span.setting-item-default-overridden'); + const defaultOverrideLabel = new SimpleIconLabel(defaultOverrideIndicator); + defaultOverrideLabel.text = localize('defaultOverriddenLabel', "Default value changed"); + return defaultOverrideIndicator; + } + + private render() { + const elementsToShow = [this.scopeOverridesElement, this.syncIgnoredElement, this.defaultOverrideIndicatorElement].filter(element => { + return element.style.display !== 'none'; + }); + + this.indicatorsContainerElement.innerText = ''; + this.indicatorsContainerElement.style.display = 'none'; + if (elementsToShow.length) { + this.indicatorsContainerElement.style.display = 'inline'; + DOM.append(this.indicatorsContainerElement, $('span', undefined, '(')); + for (let i = 0; i < elementsToShow.length - 1; i++) { + DOM.append(this.indicatorsContainerElement, elementsToShow[i]); + DOM.append(this.indicatorsContainerElement, $('span.comma', undefined, ' • ')); + } + DOM.append(this.indicatorsContainerElement, elementsToShow[elementsToShow.length - 1]); + DOM.append(this.indicatorsContainerElement, $('span', undefined, ')')); + } + } + + updateSyncIgnored(element: SettingsTreeSettingElement, ignoredSettings: string[]) { + this.syncIgnoredElement.style.display = this.userDataSyncEnablementService.isEnabled() + && ignoredSettings.includes(element.setting.key) ? 'inline' : 'none'; + this.render(); + } + + private getInlineScopeDisplayText(completeScope: string): string { + const [scope, language] = completeScope.split(':'); + const localizedScope = scope === 'user' ? + localize('user', "User") : scope === 'workspace' ? + localize('workspace', "Workspace") : localize('remote', "Remote"); + if (language) { + return `${this.languageService.getLanguageName(language)} > ${localizedScope}`; + } + return localizedScope; + } + + dispose() { + this.hover?.dispose(); + this.syncIgnoredHover?.dispose(); + } + + updateScopeOverrides(element: SettingsTreeSettingElement, elementDisposables: DisposableStore, onDidClickOverrideElement: Emitter<ISettingOverrideClickEvent>) { + this.scopeOverridesElement.innerText = ''; + this.scopeOverridesElement.style.display = 'none'; + if (element.overriddenScopeList.length || element.overriddenDefaultsLanguageList.length) { + if ((MODIFIED_INDICATOR_USE_INLINE_ONLY && element.overriddenScopeList.length) || + (element.overriddenScopeList.length === 1 && !element.overriddenDefaultsLanguageList.length)) { + // Render inline if we have the flag and there are scope overrides to render, + // or if there is only one scope override to render and no language overrides. + this.scopeOverridesElement.style.display = 'inline'; + this.hover?.dispose(); + + // Just show all the text in the label. + const prefaceText = element.isConfigured ? + localize('alsoConfiguredIn', "Also modified in") : + localize('configuredIn', "Modified in"); + this.scopeOverridesLabel.text = `${prefaceText} `; + + for (let i = 0; i < element.overriddenScopeList.length; i++) { + const overriddenScope = element.overriddenScopeList[i]; + const view = DOM.append(this.scopeOverridesElement, $('a.modified-scope', undefined, this.getInlineScopeDisplayText(overriddenScope))); + if (i !== element.overriddenScopeList.length - 1) { + DOM.append(this.scopeOverridesElement, $('span.comma', undefined, ', ')); + } + elementDisposables.add( + DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { + const [scope, language] = overriddenScope.split(':'); + onDidClickOverrideElement.fire({ + settingKey: element.setting.key, + scope: scope as ScopeString, + language + }); + e.preventDefault(); + e.stopPropagation(); + })); + } + } else if (!MODIFIED_INDICATOR_USE_INLINE_ONLY) { + // Even if the check above fails, we want to + // show the text in a custom hover only if + // the feature flag isn't on. + this.scopeOverridesElement.style.display = 'inline'; + const scopeOverridesLabelText = element.isConfigured ? + localize('alsoConfiguredElsewhere', "Also modified elsewhere") : + localize('configuredElsewhere', "Modified elsewhere"); + this.scopeOverridesLabel.text = scopeOverridesLabelText; + + let contentMarkdownString = ''; + let contentFallback = ''; + if (element.overriddenScopeList.length) { + const prefaceText = element.isConfigured ? + localize('alsoModifiedInScopes', "The setting has also been modified in the following scopes:") : + localize('modifiedInScopes', "The setting has been modified in the following scopes:"); + contentMarkdownString = prefaceText; + contentFallback = prefaceText; + for (const scope of element.overriddenScopeList) { + const scopeDisplayText = this.getInlineScopeDisplayText(scope); + contentMarkdownString += `\n- [${scopeDisplayText}](${encodeURIComponent(scope)} "${getAccessibleScopeDisplayText(scope, this.languageService)}")`; + contentFallback += `\n• ${scopeDisplayText}`; + } + } + if (element.overriddenDefaultsLanguageList.length) { + if (contentMarkdownString) { + contentMarkdownString += `\n\n`; + contentFallback += `\n\n`; + } + const prefaceText = localize('hasDefaultOverridesForLanguages', "The following languages have default overrides:"); + contentMarkdownString += prefaceText; + contentFallback += prefaceText; + for (const language of element.overriddenDefaultsLanguageList) { + const scopeDisplayText = this.languageService.getLanguageName(language); + contentMarkdownString += `\n- [${scopeDisplayText}](${encodeURIComponent(`default:${language}`)} "${scopeDisplayText}")`; + contentFallback += `\n• ${scopeDisplayText}`; + } + } + const content: ITooltipMarkdownString = { + markdown: { + value: contentMarkdownString, + isTrusted: false, + supportHtml: false + }, + markdownNotSupportedFallback: contentFallback + }; + const options: IUpdatableHoverOptions = { + linkHandler: (url: string) => { + const [scope, language] = decodeURIComponent(url).split(':'); + onDidClickOverrideElement.fire({ + settingKey: element.setting.key, + scope: scope as ScopeString, + language + }); + this.hover!.hide(); + } + }; + this.hover?.dispose(); + this.hover = setupCustomHover(this.hoverDelegate, this.scopeOverridesElement, content, options); + } + } + this.render(); + } + + updateDefaultOverrideIndicator(element: SettingsTreeSettingElement) { + this.defaultOverrideIndicatorElement.style.display = 'none'; + const sourceToDisplay = getDefaultValueSourceToDisplay(element); + if (sourceToDisplay !== undefined) { + this.defaultOverrideIndicatorElement.style.display = 'inline'; + const defaultOverrideHoverContent = localize('defaultOverriddenDetails', "Default setting value overridden by {0}", sourceToDisplay); + setupCustomHover(this.hoverDelegate, this.defaultOverrideIndicatorElement, defaultOverrideHoverContent); + } + this.render(); + } +} + +function getDefaultValueSourceToDisplay(element: SettingsTreeSettingElement): string | undefined { + let sourceToDisplay: string | undefined; + const defaultValueSource = element.defaultValueSource; + if (defaultValueSource) { + if (typeof defaultValueSource !== 'string') { + sourceToDisplay = defaultValueSource.displayName ?? defaultValueSource.id; + } else if (typeof defaultValueSource === 'string') { + sourceToDisplay = defaultValueSource; + } + } + return sourceToDisplay; +} + +function getAccessibleScopeDisplayText(completeScope: string, languageService: ILanguageService): string { + const [scope, language] = completeScope.split(':'); + const localizedScope = scope === 'user' ? + localize('user', "User") : scope === 'workspace' ? + localize('workspace', "Workspace") : localize('remote', "Remote"); + if (language) { + return localize('modifiedInScopeForLanguage', "The {0} scope for {1}", localizedScope, languageService.getLanguageName(language)); + } + return localizedScope; +} + +function getAccessibleScopeDisplayMidSentenceText(completeScope: string, languageService: ILanguageService): string { + const [scope, language] = completeScope.split(':'); + const localizedScope = scope === 'user' ? + localize('user', "User") : scope === 'workspace' ? + localize('workspace', "Workspace") : localize('remote', "Remote"); + if (language) { + return localize('modifiedInScopeForLanguageMidSentence', "the {0} scope for {1}", localizedScope.toLowerCase(), languageService.getLanguageName(language)); + } + return localizedScope; +} + +export function getIndicatorsLabelAriaLabel(element: SettingsTreeSettingElement, configurationService: IConfigurationService, languageService: ILanguageService): string { + const ariaLabelSections: string[] = []; + + // Add other overrides text + const otherOverridesStart = element.isConfigured ? + localize('alsoConfiguredIn', "Also modified in") : + localize('configuredIn', "Modified in"); + const otherOverridesList = element.overriddenScopeList + .map(scope => getAccessibleScopeDisplayMidSentenceText(scope, languageService)).join(', '); + if (element.overriddenScopeList.length) { + ariaLabelSections.push(`${otherOverridesStart} ${otherOverridesList}`); + } + + // Add sync ignored text + const ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), configurationService); + if (ignoredSettings.includes(element.setting.key)) { + ariaLabelSections.push(localize('syncIgnoredAriaLabel', "Setting ignored during sync")); + } + + // Add default override indicator text + const sourceToDisplay = getDefaultValueSourceToDisplay(element); + if (sourceToDisplay !== undefined) { + ariaLabelSections.push(localize('defaultOverriddenDetailsAriaLabel', "{0} overrides the default value", sourceToDisplay)); + } + + // Add text about default values being overridden in other languages + const otherLanguageOverridesList = element.overriddenDefaultsLanguageList + .map(language => languageService.getLanguageName(language)).join(', '); + if (element.overriddenDefaultsLanguageList.length) { + const otherLanguageOverridesText = localize('defaultOverriddenLanguagesList', "Language-specific default values exist for {0}", otherLanguageOverridesList); + ariaLabelSections.push(otherLanguageOverridesText); + } + + const ariaLabel = ariaLabelSections.join('. '); + return ariaLabel; +} diff --git a/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts index e39d23eb58e..9a1c0ec289c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts @@ -10,7 +10,7 @@ import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestCont import { localize } from 'vs/nls'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; -import { EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, GENERAL_TAG_SETTING_TAG, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, GENERAL_TAG_SETTING_TAG, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionViewItem { private readonly suggestController: SuggestController | null; @@ -135,6 +135,12 @@ export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenu localize('onlineSettingsSearch', "Online services"), localize('onlineSettingsSearchTooltip', "Show settings for online services"), '@tag:usesOnlineServices' + ), + this.createToggleAction( + 'policySettingsSearch', + localize('policySettingsSearch', "Policy services"), + localize('policySettingsSearchTooltip', "Show settings for policy services"), + `@${POLICY_SETTING_TAG}` ) ]; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 7b551ab1466..16dd1bb7a51 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -36,14 +36,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorBackground, editorErrorForeground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, editorErrorForeground, editorInfoForeground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler, attachStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { inspectSetting, ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, ObjectSettingDropdownWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester, ObjectSettingCheckboxWidget } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; -import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; +import { LANGUAGE_SETTING_TAG, POLICY_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { getDefaultIgnoredSettings, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; @@ -62,6 +62,8 @@ import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/prefere import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { focusedRowBackground, focusedRowBorder, rowHoverBackground, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; +import { getIndicatorsLabelAriaLabel, ISettingOverrideClickEvent, SettingsTreeIndicatorsLabel } from 'vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators'; +import { ILanguageService } from 'vs/editor/common/languages/language'; const $ = DOM.$; @@ -131,6 +133,14 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData ? element.defaultValue ?? {} : {}; + const elementScopeValue: Record<string, unknown> = typeof element.scopeValue === 'object' + ? element.scopeValue ?? {} + : {}; + + const data = element.isConfigured ? + { ...elementDefaultValue, ...elementScopeValue } : + elementDefaultValue; + const { objectProperties, objectPatternProperties, objectAdditionalProperties } = element.setting; const patternsAndSchemas = Object .entries(objectPatternProperties ?? {}) @@ -143,12 +153,8 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData ([key, schema]) => ({ value: key, description: schema.description }) ); - let data: Record<string, unknown> = element.value ?? {}; - if (element.setting.allKeysAreBoolean) { - // Add on default values, because we want to display all checkboxes. - data = { ...elementDefaultValue, ...data }; - } return Object.keys(data).map(key => { + const defaultValue = elementDefaultValue[key]; if (isDefined(objectProperties) && key in objectProperties) { if (element.setting.allKeysAreBoolean) { return { @@ -165,7 +171,6 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData } as IObjectDataItem; } - const defaultValue = elementDefaultValue[key]; const valueEnumOptions = getEnumOptionsFromSchema(objectProperties[key]); return { key: { @@ -183,6 +188,9 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData } as IObjectDataItem; } + // The row is removable if it doesn't have a default value assigned. + // Otherwise, it is not removable, but its value can be reset to the default. + const removable = !defaultValue; const schema = patternsAndSchemas.find(({ pattern }) => pattern.test(key))?.schema; if (schema) { const valueEnumOptions = getEnumOptionsFromSchema(schema); @@ -194,7 +202,7 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData options: valueEnumOptions, }, keyDescription: schema.description, - removable: true, + removable, } as IObjectDataItem; } @@ -212,7 +220,7 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData options: additionalValueEnums, }, keyDescription: typeof objectAdditionalProperties === 'object' ? objectAdditionalProperties.description : undefined, - removable: true, + removable, } as IObjectDataItem; }).filter(item => !isUndefinedOrNull(item.value.data)); } @@ -584,10 +592,11 @@ interface ISettingItemTemplate<T = any> extends IDisposableTemplate { containerElement: HTMLElement; categoryElement: HTMLElement; labelElement: SimpleIconLabel; + policyWarningElement: HTMLElement; descriptionElement: HTMLElement; controlElement: HTMLElement; deprecationWarningElement: HTMLElement; - miscLabel: SettingsTreeMiscLabel; + indicatorsLabel: SettingsTreeIndicatorsLabel; toolbar: ToolBar; elementDisposables: DisposableStore; } @@ -605,6 +614,7 @@ type ISettingNumberItemTemplate = ISettingTextItemTemplate; interface ISettingEnumItemTemplate extends ISettingItemTemplate<number> { selectBox: SelectBox; + selectElement: HTMLSelectElement | null; enumDescriptionElement: HTMLElement; } @@ -664,11 +674,6 @@ export interface ISettingLinkClickEvent { targetKey: string; } -export interface ISettingOverrideClickEvent { - scope: string; - targetKey: string; -} - function removeChildrenFromTabOrder(node: Element): void { const focusableElements = node.querySelectorAll(` [tabindex="0"], @@ -738,8 +743,8 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre protected readonly _onDidChangeSettingHeight = this._register(new Emitter<HeightChangeParams>()); readonly onDidChangeSettingHeight: Event<HeightChangeParams> = this._onDidChangeSettingHeight.event; - protected readonly _onApplyLanguageFilter = this._register(new Emitter<string>()); - readonly onApplyLanguageFilter: Event<string> = this._onApplyLanguageFilter.event; + protected readonly _onApplyFilter = this._register(new Emitter<string>()); + readonly onApplyFilter: Event<string> = this._onApplyFilter.event; private readonly markdownRenderer: MarkdownRenderer; @@ -774,6 +779,8 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre _container.classList.add('setting-item'); _container.classList.add('setting-item-' + typeClass); + const toDispose = new DisposableStore(); + const container = DOM.append(_container, $(AbstractSettingRenderer.CONTENTS_SELECTOR)); container.classList.add('settings-row-inner-container'); const titleElement = DOM.append(container, $('.setting-item-title')); @@ -781,8 +788,8 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const categoryElement = DOM.append(labelCategoryContainer, $('span.setting-item-category')); const labelElementContainer = DOM.append(labelCategoryContainer, $('span.setting-item-label')); const labelElement = new SimpleIconLabel(labelElementContainer); - - const miscLabel = new SettingsTreeMiscLabel(titleElement); + const indicatorsLabel = this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement); + toDispose.add(indicatorsLabel); const descriptionElement = DOM.append(container, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); @@ -793,7 +800,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); - const toDispose = new DisposableStore(); + const policyWarningElement = this.renderPolicyLabel(container, toDispose); const toolbarContainer = DOM.append(container, $('.setting-toolbar-container')); const toolbar = this.renderSettingToolbar(toolbarContainer); @@ -805,10 +812,11 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre containerElement: container, categoryElement, labelElement, + policyWarningElement, descriptionElement, controlElement, deprecationWarningElement, - miscLabel, + indicatorsLabel, toolbar }; @@ -839,6 +847,33 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre }); } + protected renderPolicyLabel(container: HTMLElement, toDispose: DisposableStore): HTMLElement { + const policyWarningElement = DOM.append(container, $('.setting-item-policy-description')); + const policyIcon = DOM.append(policyWarningElement, $('span.codicon.codicon-lock')); + toDispose.add(attachStylerCallback(this._themeService, { editorInfoForeground }, colors => { + policyIcon.style.setProperty('--organization-policy-icon-color', colors.editorInfoForeground?.toString() || ''); + })); + const element = DOM.append(policyWarningElement, $('span')); + element.textContent = localize('policyLabel', "This setting is managed by your organization."); + const viewPolicyLabel = localize('viewPolicySettings', "View policy settings"); + const linkElement: HTMLAnchorElement = DOM.append(policyWarningElement, $('a')); + linkElement.textContent = viewPolicyLabel; + linkElement.setAttribute('tabindex', '0'); + linkElement.href = '#'; + toDispose.add(DOM.addStandardDisposableListener(linkElement, DOM.EventType.CLICK, (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this._onApplyFilter.fire(`@${POLICY_SETTING_TAG}`); + })); + toDispose.add(DOM.addStandardDisposableListener(linkElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) { + e.stopPropagation(); + this._onApplyFilter.fire(`@${POLICY_SETTING_TAG}`); + } + })); + return policyWarningElement; + } + protected renderSettingToolbar(container: HTMLElement): ToolBar { const toggleMenuKeybinding = this._keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU); let toggleMenuTitle = localize('settingsContextMenuTitle', "More Actions... "); @@ -885,7 +920,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.descriptionElement.innerText = element.description; } - template.miscLabel.updateOtherOverrides(element, template.elementDisposables, this._onDidClickOverrideElement); + template.indicatorsLabel.updateScopeOverrides(element, template.elementDisposables, this._onDidClickOverrideElement); const onChange = (value: any) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context!.valueType, manualReset: false }); const deprecationText = element.setting.deprecationMessage || ''; @@ -902,12 +937,14 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre this.renderValue(element, <ISettingItemTemplate>template, onChange); - template.miscLabel.updateSyncIgnored(element, this.ignoredSettings); - template.miscLabel.updateDefaultOverrideIndicator(element); + template.indicatorsLabel.updateSyncIgnored(element, this.ignoredSettings); + template.indicatorsLabel.updateDefaultOverrideIndicator(element); template.elementDisposables.add(this.onDidChangeIgnoredSettings(() => { - template.miscLabel.updateSyncIgnored(element, this.ignoredSettings); + template.indicatorsLabel.updateSyncIgnored(element, this.ignoredSettings); })); + template.policyWarningElement.hidden = !element.hasPolicyValue; + this.updateSettingTabbable(element, template); template.elementDisposables.add(element.onDidChangeTabbable(() => { this.updateSettingTabbable(element, template); @@ -1087,7 +1124,7 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I template.elementDisposables.add(template.button.onDidClick(() => { if (isLanguageTagSetting) { - this._onApplyLanguageFilter.fire(plainKey); + this._onApplyFilter.fire(`@${LANGUAGE_SETTING_TAG}${plainKey}`); } else { this._onDidOpenSettings.fire(dataElement.setting.key); } @@ -1138,9 +1175,7 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr common.toDispose.add( listWidget.onDidChangeList(e => { const newList = this.computeNewList(template, e); - if (template.onChange) { - template.onChange(newList); - } + template.onChange?.(newList); }) ); @@ -1308,22 +1343,22 @@ abstract class AbstractSettingObjectRenderer extends AbstractSettingRenderer imp newItems.push(e.item); } + Object.entries(newValue).forEach(([key, value]) => { + // value from the scope has changed back to the default + if (scopeValue[key] !== value && defaultValue[key] === value) { + delete newValue[key]; + } + }); + + const newObject = Object.keys(newValue).length === 0 ? undefined : newValue; + if (template.objectCheckboxWidget) { - Object.entries(newValue).forEach(([key, value]) => { - // A value from the scope has changed back to the default. - // For the bool object renderer, we don't want to save these values. - if (scopeValue[key] !== value && defaultValue[key] === value) { - delete newValue[key]; - } - }); template.objectCheckboxWidget.setValue(newItems); } else { template.objectDropdownWidget!.setValue(newItems); } - if (template.onChange) { - template.onChange(newValue); - } + template.onChange?.(newObject); } } @@ -1511,9 +1546,7 @@ abstract class AbstractSettingTextRenderer extends AbstractSettingRenderer imple })); common.toDispose.add( inputBox.onDidChange(e => { - if (template.onChange) { - template.onChange(e); - } + template.onChange?.(e); })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); @@ -1538,6 +1571,7 @@ abstract class AbstractSettingTextRenderer extends AbstractSettingRenderer imple template.onChange = undefined; template.inputBox.value = dataElement.value; template.inputBox.setAriaLabel(dataElement.setting.key); + template.inputBox.inputElement.disabled = !!dataElement.hasPolicyValue; template.onChange = value => { if (!renderValidations(dataElement, template, false)) { onChange(value); @@ -1623,9 +1657,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre common.toDispose.add( selectBox.onDidSelect(e => { - if (template.onChange) { - template.onChange(e.index); - } + template.onChange?.(e.index); })); const enumDescriptionElement = common.containerElement.insertBefore($('.setting-item-enumDescription'), common.descriptionElement.nextSibling); @@ -1633,6 +1665,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const template: ISettingEnumItemTemplate = { ...common, selectBox, + selectElement, enumDescriptionElement }; @@ -1704,6 +1737,10 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre } }; + if (template.selectElement) { + template.selectElement.disabled = !!dataElement.hasPolicyValue; + } + template.enumDescriptionElement.innerText = ''; } } @@ -1724,9 +1761,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT })); common.toDispose.add( inputBox.onDidChange(e => { - if (template.onChange) { - template.onChange(e); - } + template.onChange?.(e); })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); @@ -1757,6 +1792,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT template.onChange = undefined; template.inputBox.value = dataElement.value; template.inputBox.setAriaLabel(dataElement.setting.key); + template.inputBox.setEnabled(!dataElement.hasPolicyValue); template.onChange = value => { if (!renderValidations(dataElement, template, false)) { onChange(nullNumParseFn(value)); @@ -1781,7 +1817,7 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); const labelElementContainer = DOM.append(titleElement, $('span.setting-item-label')); const labelElement = new SimpleIconLabel(labelElementContainer); - const miscLabel = new SettingsTreeMiscLabel(titleElement); + const indicatorsLabel = this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement); const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description')); const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); @@ -1789,7 +1825,6 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); modifiedIndicatorElement.title = localize('modified', "The setting has been configured in the current scope."); - const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); const toDispose = new DisposableStore(); @@ -1819,6 +1854,8 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const toolbar = this.renderSettingToolbar(toolbarContainer); toDispose.add(toolbar); + const policyWarningElement = this.renderPolicyLabel(container, toDispose); + const template: ISettingBoolItemTemplate = { toDispose, elementDisposables: new DisposableStore(), @@ -1828,9 +1865,10 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre labelElement, controlElement, checkbox, + policyWarningElement, descriptionElement, deprecationWarningElement, - miscLabel, + indicatorsLabel, toolbar }; @@ -1852,6 +1890,11 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre template.onChange = undefined; template.checkbox.checked = dataElement.value; template.checkbox.setTitle(dataElement.setting.key); + if (dataElement.hasPolicyValue) { + template.checkbox.disable(); + } else { + template.checkbox.enable(); + } template.onChange = onChange; } } @@ -1912,7 +1955,7 @@ export class SettingTreeRenderers { readonly onDidChangeSettingHeight: Event<HeightChangeParams>; - readonly onApplyLanguageFilter: Event<string>; + readonly onApplyFilter: Event<string>; readonly allRenderers: ITreeRenderer<SettingsTreeElement, never, any>[]; @@ -1961,7 +2004,7 @@ export class SettingTreeRenderers { this.onDidClickSettingLink = Event.any(...settingRenderers.map(r => r.onDidClickSettingLink)); this.onDidFocusSetting = Event.any(...settingRenderers.map(r => r.onDidFocusSetting)); this.onDidChangeSettingHeight = Event.any(...settingRenderers.map(r => r.onDidChangeSettingHeight)); - this.onApplyLanguageFilter = Event.any(...settingRenderers.map(r => r.onApplyLanguageFilter)); + this.onApplyFilter = Event.any(...settingRenderers.map(r => r.onApplyFilter)); this.allRenderers = [ ...settingRenderers, @@ -2097,120 +2140,6 @@ function escapeInvisibleChars(enumValue: string): string { .replace(/\r/g, '\\r'); } -/** - * Controls logic and rendering of the label next to each setting header. - * For example, the "Modified by" and "Overridden by" labels go here. - */ -class SettingsTreeMiscLabel { - private labelElement: HTMLElement; - private otherOverridesElement: HTMLElement; - private syncIgnoredElement: HTMLElement; - private defaultOverrideIndicatorElement: HTMLElement; - private defaultOverrideIndicatorLabel: SimpleIconLabel; - - constructor(container: HTMLElement) { - this.labelElement = DOM.append(container, $('.misc-label')); - this.labelElement.style.display = 'inline'; - - this.otherOverridesElement = this.createOtherOverridesElement(); - this.syncIgnoredElement = this.createSyncIgnoredElement(); - const { element, label } = this.createDefaultOverrideIndicator(); - this.defaultOverrideIndicatorElement = element; - this.defaultOverrideIndicatorLabel = label; - } - - private createOtherOverridesElement(): HTMLElement { - const otherOverridesElement = $('span.setting-item-overrides'); - return otherOverridesElement; - } - - private createSyncIgnoredElement(): HTMLElement { - const syncIgnoredElement = $('span.setting-item-ignored'); - const syncIgnoredLabel = new SimpleIconLabel(syncIgnoredElement); - syncIgnoredLabel.text = `$(sync-ignored) ${localize('extensionSyncIgnoredLabel', 'Sync: Ignored')}`; - syncIgnoredLabel.title = localize('syncIgnoredTitle', "Settings sync does not sync this setting"); - return syncIgnoredElement; - } - - private createDefaultOverrideIndicator(): { element: HTMLElement; label: SimpleIconLabel } { - const defaultOverrideIndicator = $('span.setting-item-default-overridden'); - const defaultOverrideLabel = new SimpleIconLabel(defaultOverrideIndicator); - return { element: defaultOverrideIndicator, label: defaultOverrideLabel }; - } - - private render() { - const elementsToShow = [this.otherOverridesElement, this.syncIgnoredElement, this.defaultOverrideIndicatorElement].filter(element => { - return element.style.display !== 'none'; - }); - - this.labelElement.innerText = ''; - this.labelElement.style.display = 'none'; - if (elementsToShow.length) { - this.labelElement.style.display = 'inline'; - DOM.append(this.labelElement, $('span', undefined, '(')); - for (let i = 0; i < elementsToShow.length - 1; i++) { - DOM.append(this.labelElement, elementsToShow[i]); - DOM.append(this.labelElement, $('span.comma', undefined, ', ')); - } - DOM.append(this.labelElement, elementsToShow[elementsToShow.length - 1]); - DOM.append(this.labelElement, $('span', undefined, ')')); - } - } - - updateSyncIgnored(element: SettingsTreeSettingElement, ignoredSettings: string[]) { - this.syncIgnoredElement.style.display = ignoredSettings.includes(element.setting.key) ? 'inline' : 'none'; - this.render(); - } - - updateOtherOverrides(element: SettingsTreeSettingElement, elementDisposables: DisposableStore, onDidClickOverrideElement: Emitter<ISettingOverrideClickEvent>) { - this.otherOverridesElement.innerText = ''; - this.otherOverridesElement.style.display = 'none'; - if (element.overriddenScopeList.length) { - this.otherOverridesElement.style.display = 'inline'; - const otherOverridesLabel = element.isConfigured ? - localize('alsoConfiguredIn', "Also modified in") : - localize('configuredIn', "Modified in"); - - DOM.append(this.otherOverridesElement, $('span', undefined, `${otherOverridesLabel}: `)); - - for (let i = 0; i < element.overriddenScopeList.length; i++) { - const view = DOM.append(this.otherOverridesElement, $('a.modified-scope', undefined, element.overriddenScopeList[i])); - - if (i !== element.overriddenScopeList.length - 1) { - DOM.append(this.otherOverridesElement, $('span', undefined, ', ')); - } - - elementDisposables.add( - DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { - onDidClickOverrideElement.fire({ - targetKey: element.setting.key, - scope: element.overriddenScopeList[i] - }); - e.preventDefault(); - e.stopPropagation(); - })); - } - } - this.render(); - } - - updateDefaultOverrideIndicator(element: SettingsTreeSettingElement) { - this.defaultOverrideIndicatorElement.style.display = 'none'; - if (element.setting.defaultValueSource) { - this.defaultOverrideIndicatorElement.style.display = 'inline'; - const defaultValueSource = element.setting.defaultValueSource; - if (typeof defaultValueSource !== 'string' && defaultValueSource.id !== element.setting.extensionInfo?.id) { - const extensionSource = defaultValueSource.displayName ?? defaultValueSource.id; - this.defaultOverrideIndicatorLabel.title = localize('defaultOverriddenDetails', "Default value overridden by {0}", extensionSource); - this.defaultOverrideIndicatorLabel.text = localize('defaultOverrideLabelText', "$(wrench) Overridden by: {0}", extensionSource); - } else if (typeof defaultValueSource === 'string') { - this.defaultOverrideIndicatorLabel.title = localize('defaultOverriddenDetails', "Default value overridden by {0}", defaultValueSource); - this.defaultOverrideIndicatorLabel.text = localize('defaultOverrideLabelText', "$(wrench) Overridden by: {0}", defaultValueSource); - } - } - this.render(); - } -} export class SettingsTreeFilter implements ITreeFilter<SettingsTreeElement> { constructor( @@ -2361,7 +2290,7 @@ export class NonCollapsibleObjectTreeModel<T> extends ObjectTreeModel<T> { } class SettingsTreeAccessibilityProvider implements IListAccessibilityProvider<SettingsTreeElement> { - constructor(private readonly configurationService: IConfigurationService) { + constructor(private readonly configurationService: IConfigurationService, private readonly languageService: ILanguageService) { } getAriaLabel(element: SettingsTreeElement) { @@ -2374,9 +2303,9 @@ class SettingsTreeAccessibilityProvider implements IListAccessibilityProvider<Se ariaLabelSections.push(modifiedText); } - const miscLabelAriaLabel = this.getMiscLabelAriaLabel(element); - if (miscLabelAriaLabel.length) { - ariaLabelSections.push(`${miscLabelAriaLabel}.`); + const indicatorsLabelAriaLabel = getIndicatorsLabelAriaLabel(element, this.configurationService, this.languageService); + if (indicatorsLabelAriaLabel.length) { + ariaLabelSections.push(`${indicatorsLabelAriaLabel}.`); } const descriptionWithoutSettingLinks = fixSettingLinks(element.description, false); @@ -2394,39 +2323,6 @@ class SettingsTreeAccessibilityProvider implements IListAccessibilityProvider<Se getWidgetAriaLabel() { return localize('settings', "Settings"); } - - private getMiscLabelAriaLabel(element: SettingsTreeSettingElement): string { - const ariaLabelSections: string[] = []; - - // Add other overrides text - const otherOverridesStart = element.isConfigured ? - localize('alsoConfiguredIn', "Also modified in") : - localize('configuredIn', "Modified in"); - const otherOverridesList = element.overriddenScopeList.join(', '); - if (element.overriddenScopeList.length) { - ariaLabelSections.push(`${otherOverridesStart} ${otherOverridesList}`); - } - - // Add sync ignored text - const ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this.configurationService); - if (ignoredSettings.includes(element.setting.key)) { - ariaLabelSections.push(localize('syncIgnoredTitle', "Settings sync does not sync this setting")); - } - - // Add default override indicator text - if (element.setting.defaultValueSource) { - const defaultValueSource = element.setting.defaultValueSource; - if (typeof defaultValueSource !== 'string' && defaultValueSource.id !== element.setting.extensionInfo?.id) { - const extensionSource = defaultValueSource.displayName ?? defaultValueSource.id; - ariaLabelSections.push(localize('defaultOverriddenDetails', "Default value overridden by {0}", extensionSource)); - } else if (typeof defaultValueSource === 'string') { - ariaLabelSections.push(localize('defaultOverriddenDetails', "Default value overridden by {0}", defaultValueSource)); - } - } - - const ariaLabel = ariaLabelSections.join('. '); - return ariaLabel; - } } export class SettingsTree extends WorkbenchObjectTree<SettingsTreeElement> { @@ -2441,6 +2337,7 @@ export class SettingsTree extends WorkbenchObjectTree<SettingsTreeElement> { @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService, @IInstantiationService instantiationService: IInstantiationService, + @ILanguageService languageService: ILanguageService ) { super('SettingsTree', container, new SettingsTreeDelegate(), @@ -2453,7 +2350,7 @@ export class SettingsTree extends WorkbenchObjectTree<SettingsTreeElement> { return e.id; } }, - accessibilityProvider: new SettingsTreeAccessibilityProvider(configurationService), + accessibilityProvider: new SettingsTreeAccessibilityProvider(configurationService, languageService), styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), filter: instantiationService.createInstance(SettingsTreeFilter, viewState), smoothScrolling: configurationService.getValue<boolean>('workbench.list.smoothScrolling'), diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index e41f7298554..8f7f3f598b8 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -7,20 +7,20 @@ import * as arrays from 'vs/base/common/arrays'; import { escapeRegExpCharacters, isFalsyOrWhitespace } from 'vs/base/common/strings'; import { isArray, withUndefinedAsNull, isUndefinedOrNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { localize } from 'vs/nls'; import { ConfigurationTarget, IConfigurationValue } from 'vs/platform/configuration/common/configuration'; import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { ITOCEntry, knownAcronyms, knownTermMappings, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { IExtensionSetting, ISearchResult, ISetting, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, LOCAL_MACHINE_PROFILE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; -import { ConfigurationScope, EditPresentationTypes, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope, EditPresentationTypes, Extensions, IConfigurationRegistry, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices'; @@ -130,6 +130,12 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { defaultValue?: any; /** + * The source of the default value to display. + * This value also accounts for extension-contributed language-specific default value overrides. + */ + defaultValueSource: string | IExtensionInfo | undefined; + + /** * Whether the setting is configured in the selected scope. */ isConfigured = false; @@ -139,9 +145,20 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { */ isUntrusted = false; + /** + * Whether the setting is under a policy that blocks all changes. + */ + hasPolicyValue = false; + tags?: Set<string>; overriddenScopeList: string[] = []; + overriddenDefaultsLanguageList: string[] = []; + + /** + * For each language that contributes setting values or default overrides, we can see those values here. + */ languageOverrideValues: Map<string, IConfigurationValue<unknown>> = new Map<string, IConfigurationValue<unknown>>(); + description!: string; valueType!: SettingValueType; @@ -150,7 +167,8 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { parent: SettingsTreeGroupElement, inspectResult: IInspectResult, isWorkspaceTrusted: boolean, - private readonly languageService: ILanguageService + private readonly languageService: ILanguageService, + private readonly userDataProfileService: IUserDataProfileService, ) { super(sanitizeId(parent.id + '_' + setting.key)); this.setting = setting; @@ -182,7 +200,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } update(inspectResult: IInspectResult, isWorkspaceTrusted: boolean): void { - const { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector } = inspectResult; + let { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector } = inspectResult; switch (targetSelector) { case 'workspaceFolderValue': @@ -193,65 +211,90 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { let displayValue = isConfigured ? inspected[targetSelector] : inspected.defaultValue; const overriddenScopeList: string[] = []; - if (targetSelector !== 'workspaceValue' && typeof inspected.workspaceValue !== 'undefined') { - overriddenScopeList.push(localize('workspace', "Workspace")); + const overriddenDefaultsLanguageList: string[] = []; + if ((languageSelector || targetSelector !== 'workspaceValue') && typeof inspected.workspaceValue !== 'undefined') { + overriddenScopeList.push('workspace:'); } - - if (targetSelector !== 'userRemoteValue' && typeof inspected.userRemoteValue !== 'undefined') { - overriddenScopeList.push(localize('remote', "Remote")); + if ((languageSelector || targetSelector !== 'userRemoteValue') && typeof inspected.userRemoteValue !== 'undefined') { + overriddenScopeList.push('remote:'); } - - if (targetSelector !== 'userLocalValue' && typeof inspected.userLocalValue !== 'undefined') { - overriddenScopeList.push(localize('user', "User")); + if ((languageSelector || targetSelector !== 'userLocalValue') && typeof inspected.userLocalValue !== 'undefined') { + overriddenScopeList.push('user:'); } if (inspected.overrideIdentifiers) { for (const overrideIdentifier of inspected.overrideIdentifiers) { const inspectedOverride = inspectedLanguageOverrides.get(overrideIdentifier); if (inspectedOverride) { + if (this.languageService.isRegisteredLanguageId(overrideIdentifier)) { + if (languageSelector !== overrideIdentifier && typeof inspectedOverride.default?.override !== 'undefined') { + overriddenDefaultsLanguageList.push(overrideIdentifier); + } + if ((languageSelector !== overrideIdentifier || targetSelector !== 'workspaceValue') && typeof inspectedOverride.workspace?.override !== 'undefined') { + overriddenScopeList.push(`workspace:${overrideIdentifier}`); + } + if ((languageSelector !== overrideIdentifier || targetSelector !== 'userRemoteValue') && typeof inspectedOverride.userRemote?.override !== 'undefined') { + overriddenScopeList.push(`remote:${overrideIdentifier}`); + } + if ((languageSelector !== overrideIdentifier || targetSelector !== 'userLocalValue') && typeof inspectedOverride.userLocal?.override !== 'undefined') { + overriddenScopeList.push(`user:${overrideIdentifier}`); + } + } this.languageOverrideValues.set(overrideIdentifier, inspectedOverride); } } } + this.overriddenScopeList = overriddenScopeList; + this.overriddenDefaultsLanguageList = overriddenDefaultsLanguageList; - if (languageSelector && this.languageOverrideValues.has(languageSelector)) { + // The user might have added, removed, or modified a language filter, + // so we reset the default value source to the non-language-specific default value source for now. + this.defaultValueSource = this.setting.nonLanguageSpecificDefaultValueSource; + + if (inspected.policyValue) { + this.hasPolicyValue = true; + isConfigured = false; // The user did not manually configure the setting themselves. + displayValue = inspected.policyValue; + this.scopeValue = inspected.policyValue; + this.defaultValue = inspected.defaultValue; + } else if (languageSelector && this.languageOverrideValues.has(languageSelector)) { const overrideValues = this.languageOverrideValues.get(languageSelector)!; // In the worst case, go back to using the previous display value. // Also, sometimes the override is in the form of a default value override, so consider that second. displayValue = (isConfigured ? overrideValues[targetSelector] : overrideValues.defaultValue) ?? displayValue; - this.value = displayValue; this.scopeValue = isConfigured && overrideValues[targetSelector]; this.defaultValue = overrideValues.defaultValue ?? inspected.defaultValue; const registryValues = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationDefaultsOverrides(); const overrideValueSource = registryValues.get(`[${languageSelector}]`)?.valuesSources?.get(this.setting.key); if (overrideValueSource) { - this.setting.defaultValueSource = overrideValueSource; + this.defaultValueSource = overrideValueSource; } } else { - this.value = displayValue; this.scopeValue = isConfigured && inspected[targetSelector]; this.defaultValue = inspected.defaultValue; } + this.value = displayValue; this.isConfigured = isConfigured; - if (isConfigured || this.setting.tags || this.tags || this.setting.restricted) { + if (isConfigured || this.setting.tags || this.tags || this.setting.restricted || this.hasPolicyValue) { // Don't create an empty Set for all 1000 settings, only if needed this.tags = new Set<string>(); if (isConfigured) { this.tags.add(MODIFIED_SETTING_TAG); } - if (this.setting.tags) { - this.setting.tags.forEach(tag => this.tags!.add(tag)); - } + this.setting.tags?.forEach(tag => this.tags!.add(tag)); if (this.setting.restricted) { this.tags.add(REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG); } + + if (this.hasPolicyValue) { + this.tags.add(POLICY_SETTING_TAG); + } } - this.overriddenScopeList = overriddenScopeList; if (this.setting.description.length > SettingsTreeSettingElement.MAX_DESC_LINES) { const truncatedDescLines = this.setting.description.slice(0, SettingsTreeSettingElement.MAX_DESC_LINES); truncatedDescLines.push('[...]'); @@ -335,8 +378,13 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { return REMOTE_MACHINE_SCOPES.indexOf(this.setting.scope) !== -1; } - if (configTarget === ConfigurationTarget.USER_LOCAL && isRemote) { - return LOCAL_MACHINE_SCOPES.indexOf(this.setting.scope) !== -1; + if (configTarget === ConfigurationTarget.USER_LOCAL) { + if (!this.userDataProfileService.currentProfile.isDefault) { + return LOCAL_MACHINE_PROFILE_SCOPES.indexOf(this.setting.scope) !== -1; + } + if (isRemote) { + return LOCAL_MACHINE_SCOPES.indexOf(this.setting.scope) !== -1; + } } return true; @@ -423,7 +471,8 @@ export class SettingsTreeModel { protected _viewState: ISettingsEditorViewState, private _isWorkspaceTrusted: boolean, @IWorkbenchConfigurationService private readonly _configurationService: IWorkbenchConfigurationService, - @ILanguageService private readonly _languageService: ILanguageService + @ILanguageService private readonly _languageService: ILanguageService, + @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService, ) { } @@ -453,7 +502,7 @@ export class SettingsTreeModel { } private disposeChildren(children: SettingsTreeGroupChild[]) { - for (let child of children) { + for (const child of children) { this.recursiveDispose(child); } } @@ -521,7 +570,7 @@ export class SettingsTreeModel { private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement { const inspectResult = inspectSetting(setting.key, this._viewState.settingsTarget, this._viewState.languageFilter, this._configurationService); - const element = new SettingsTreeSettingElement(setting, parent, inspectResult, this._isWorkspaceTrusted, this._languageService); + const element = new SettingsTreeSettingElement(setting, parent, inspectResult, this._isWorkspaceTrusted, this._languageService, this._userDataProfileService); const nameElements = this._treeElementsBySettingName.get(setting.key) || []; nameElements.push(element); @@ -764,9 +813,10 @@ export class SearchResultModel extends SettingsTreeModel { isWorkspaceTrusted: boolean, @IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService, @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, - @ILanguageService languageService: ILanguageService + @ILanguageService languageService: ILanguageService, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, ) { - super(viewState, isWorkspaceTrusted, configurationService, languageService); + super(viewState, isWorkspaceTrusted, configurationService, languageService, userDataProfileService); this.update({ id: 'searchResultModel', label: '' }); } @@ -781,9 +831,7 @@ export class SearchResultModel extends SettingsTreeModel { const localMatchKeys = new Set(); const localResult = this.rawSearchResults[SearchResultIdx.Local]; - if (localResult) { - localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key)); - } + localResult?.filterMatches.forEach(m => localMatchKeys.add(m.setting.key)); const remoteResult = this.rawSearchResults[SearchResultIdx.Remote]; if (remoteResult) { @@ -902,6 +950,11 @@ export function parseQuery(query: string): IParsedQuery { return ''; }); + query = query.replace(`@${POLICY_SETTING_TAG}`, () => { + tags.push(POLICY_SETTING_TAG); + return ''; + }); + const extensions: string[] = []; const features: string[] = []; const ids: string[] = []; diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index 6138098d36f..39a4cc0d044 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -56,6 +56,7 @@ export const CONTEXT_WHEN_FOCUS = new RawContextKey<boolean>('whenFocus', false) export const KEYBINDINGS_EDITOR_COMMAND_SEARCH = 'keybindings.editor.searchKeybindings'; export const KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS = 'keybindings.editor.clearSearchResults'; +export const KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_HISTORY = 'keybindings.editor.clearSearchHistory'; export const KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS = 'keybindings.editor.recordSearchKeys'; export const KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE = 'keybindings.editor.toggleSortByPrecedence'; export const KEYBINDINGS_EDITOR_COMMAND_DEFINE = 'keybindings.editor.defineKeybinding'; @@ -78,8 +79,10 @@ export const FEATURE_SETTING_TAG = 'feature:'; export const ID_SETTING_TAG = 'id:'; export const LANGUAGE_SETTING_TAG = 'lang:'; export const GENERAL_TAG_SETTING_TAG = 'tag:'; +export const POLICY_SETTING_TAG = 'hasPolicy'; export const WORKSPACE_TRUST_SETTING_TAG = 'workspaceTrust'; export const REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG = 'requireTrustedWorkspace'; export const KEYBOARD_LAYOUT_OPEN_PICKER = 'workbench.action.openKeyboardLayoutPicker'; export const ENABLE_LANGUAGE_FILTER = true; +export const MODIFIED_INDICATOR_USE_INLINE_ONLY = false; diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index 0a210e04734..be64900c807 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -13,7 +13,6 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -24,6 +23,7 @@ import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEdit import { RegisteredEditorPriority, IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { ITextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IPreferencesService, USE_SPLIT_JSON_SETTING } from 'vs/workbench/services/preferences/common/preferences'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; const schemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution); @@ -36,7 +36,7 @@ export class PreferencesContribution implements IWorkbenchContribution { @ITextModelService private readonly textModelResolverService: ITextModelService, @IPreferencesService private readonly preferencesService: IPreferencesService, @ILanguageService private readonly languageService: ILanguageService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @@ -71,7 +71,7 @@ export class PreferencesContribution implements IWorkbenchContribution { }, ({ resource, options }): EditorInputWithOptions => { // Global User Settings File - if (isEqual(resource, this.environmentService.settingsResource)) { + if (isEqual(resource, this.userDataProfileService.currentProfile.settingsResource)) { return { editor: this.preferencesService.createSplitJsonEditorInput(ConfigurationTarget.USER_LOCAL, resource), options }; } diff --git a/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts b/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts index 20817d82a5c..085f1b548b3 100644 --- a/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts +++ b/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts @@ -11,10 +11,10 @@ import { Position } from 'vs/editor/common/core/position'; suite('SmartSnippetInserter', () => { function testSmartSnippetInserter(text: string[], runner: (assert: (desiredPos: Position, pos: Position, prepend: string, append: string) => void) => void): void { - let model = createTextModel(text.join('\n')); + const model = createTextModel(text.join('\n')); runner((desiredPos, pos, prepend, append) => { - let actual = SmartSnippetInserter.insertSnippet(model, desiredPos); - let expected = { + const actual = SmartSnippetInserter.insertSnippet(model, desiredPos); + const expected = { position: pos, prepend, append diff --git a/src/vs/workbench/contrib/profiles/common/profiles.contribution.ts b/src/vs/workbench/contrib/profiles/common/profiles.contribution.ts deleted file mode 100644 index facfa51c4a3..00000000000 --- a/src/vs/workbench/contrib/profiles/common/profiles.contribution.ts +++ /dev/null @@ -1,6 +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 './profilesActions'; diff --git a/src/vs/workbench/contrib/profiles/common/profilesActions.ts b/src/vs/workbench/contrib/profiles/common/profilesActions.ts deleted file mode 100644 index bae901d966d..00000000000 --- a/src/vs/workbench/contrib/profiles/common/profilesActions.ts +++ /dev/null @@ -1,136 +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 { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { joinPath } from 'vs/base/common/resources'; -import { localize } from 'vs/nls'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IFileService } from 'vs/platform/files/common/files'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; -import { IProfile, isProfile, IWorkbenchProfileService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER } from 'vs/workbench/services/profiles/common/profile'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; - -registerAction2(class ExportProfileAction extends Action2 { - constructor() { - super({ - id: 'workbench.profiles.actions.exportProfile', - title: { - value: localize('export profile', "Export Settings as a Profile..."), - original: 'Export Settings as a Profile...' - }, - category: PROFILES_CATEGORY, - f1: true - }); - } - - async run(accessor: ServicesAccessor) { - const textFileService = accessor.get(ITextFileService); - const fileDialogService = accessor.get(IFileDialogService); - const profileService = accessor.get(IWorkbenchProfileService); - const notificationService = accessor.get(INotificationService); - - const profileLocation = await fileDialogService.showSaveDialog({ - title: localize('export profile dialog', "Save Profile"), - filters: PROFILE_FILTER, - defaultUri: joinPath(await fileDialogService.defaultFilePath(), `profile.${PROFILE_EXTENSION}`), - }); - - if (!profileLocation) { - return; - } - - const profile = await profileService.createProfile({ skipComments: true }); - await textFileService.create([{ resource: profileLocation, value: JSON.stringify(profile), options: { overwrite: true } }]); - - notificationService.info(localize('export success', "{0}: Exported successfully.", PROFILES_CATEGORY)); - } -}); - -registerAction2(class ImportProfileAction extends Action2 { - constructor() { - super({ - id: 'workbench.profiles.actions.importProfile', - title: { - value: localize('import profile', "Import Settings from a Profile..."), - original: 'Import Settings from a Profile...' - }, - category: PROFILES_CATEGORY, - f1: true - }); - } - - async run(accessor: ServicesAccessor) { - const fileDialogService = accessor.get(IFileDialogService); - const quickInputService = accessor.get(IQuickInputService); - const fileService = accessor.get(IFileService); - const requestService = accessor.get(IRequestService); - const profileService = accessor.get(IWorkbenchProfileService); - const dialogService = accessor.get(IDialogService); - - if (!(await dialogService.confirm({ - title: localize('import profile title', "Import Settings from a Profile"), - message: localize('confiirmation message', "This will replace your current settings. Are you sure you want to continue?"), - })).confirmed) { - return; - } - - const disposables = new DisposableStore(); - const quickPick = disposables.add(quickInputService.createQuickPick()); - const updateQuickPickItems = (value?: string) => { - const selectFromFileItem: IQuickPickItem = { label: localize('select from file', "Import from profile file") }; - quickPick.items = value ? [{ label: localize('select from url', "Import from URL"), description: quickPick.value }, selectFromFileItem] : [selectFromFileItem]; - }; - quickPick.title = localize('import profile quick pick title', "Import Settings from a Profile"); - quickPick.placeholder = localize('import profile placeholder', "Provide profile URL or select profile file to import"); - quickPick.ignoreFocusOut = true; - disposables.add(quickPick.onDidChangeValue(updateQuickPickItems)); - updateQuickPickItems(); - quickPick.matchOnLabel = false; - quickPick.matchOnDescription = false; - disposables.add(quickPick.onDidAccept(async () => { - quickPick.hide(); - const profile = quickPick.selectedItems[0].description ? await this.getProfileFromURL(quickPick.value, requestService) : await this.getProfileFromFileSystem(fileDialogService, fileService); - if (profile) { - await profileService.setProfile(profile); - } - })); - disposables.add(quickPick.onDidHide(() => disposables.dispose())); - quickPick.show(); - } - - private async getProfileFromFileSystem(fileDialogService: IFileDialogService, fileService: IFileService): Promise<IProfile | null> { - const profileLocation = await fileDialogService.showOpenDialog({ - canSelectFolders: false, - canSelectFiles: true, - canSelectMany: false, - filters: PROFILE_FILTER, - title: localize('import profile dialog', "Import Profile"), - }); - if (!profileLocation) { - return null; - } - const content = (await fileService.readFile(profileLocation[0])).value.toString(); - const parsed = JSON.parse(content); - return isProfile(parsed) ? parsed : null; - } - - private async getProfileFromURL(url: string, requestService: IRequestService): Promise<IProfile | null> { - const options = { type: 'GET', url }; - const context = await requestService.request(options, CancellationToken.None); - if (context.res.statusCode === 200) { - const result = await asJson(context); - return isProfile(result) ? result : null; - } else { - const message = await asText(context); - throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`); - } - } - -}); diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts index 2eb6197aa6a..85a5e04afd2 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -24,7 +24,7 @@ quickAccessRegistry.registerQuickAccessProvider({ ctor: HelpQuickAccessProvider, prefix: HelpQuickAccessProvider.PREFIX, placeholder: localize('helpQuickAccessPlaceholder', "Type '{0}' to get help on the actions you can take from here.", HelpQuickAccessProvider.PREFIX), - helpEntries: [{ description: localize('helpQuickAccess', "Show all Quick Access Providers"), needsEditor: false }] + helpEntries: [{ description: localize('helpQuickAccess', "Show all Quick Access Providers") }] }); quickAccessRegistry.registerQuickAccessProvider({ @@ -32,7 +32,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: ViewQuickAccessProvider.PREFIX, contextKey: 'inViewsPicker', placeholder: localize('viewQuickAccessPlaceholder', "Type the name of a view, output channel or terminal to open."), - helpEntries: [{ description: localize('viewQuickAccess', "Open View"), needsEditor: false }] + helpEntries: [{ description: localize('viewQuickAccess', "Open View"), commandId: OpenViewPickerAction.ID }] }); quickAccessRegistry.registerQuickAccessProvider({ @@ -40,7 +40,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: CommandsQuickAccessProvider.PREFIX, contextKey: 'inCommandsPicker', placeholder: localize('commandsQuickAccessPlaceholder', "Type the name of a command to run."), - helpEntries: [{ description: localize('commandsQuickAccess', "Show and Run Commands"), needsEditor: false }] + helpEntries: [{ description: localize('commandsQuickAccess', "Show and Run Commands"), commandId: ShowAllCommandsAction.ID }] }); //#endregion diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index 4a278af2146..1ee86f9e52c 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -21,6 +21,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IDebugService, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; interface IViewQuickPickItem extends IPickerQuickAccessItem { containerLabel: string; @@ -36,6 +37,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider<IViewQuic @IOutputService private readonly outputService: IOutputService, @ITerminalService private readonly terminalService: ITerminalService, @ITerminalGroupService private readonly terminalGroupService: ITerminalGroupService, + @IDebugService private readonly debugService: IDebugService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IContextKeyService private readonly contextKeyService: IContextKeyService ) { @@ -181,6 +183,23 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider<IViewQuic }); }); + // Debug Consoles + this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl()).forEach((session, _) => { + const label = session.name; + viewEntries.push({ + label, + containerLabel: localize('debugConsoles', "Debug Console"), + accept: async () => { + await this.debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); + + if (!this.viewsService.isViewVisible(REPL_VIEW_ID)) { + await this.viewsService.openView(REPL_VIEW_ID, true); + } + } + }); + + }); + // Output Channels const channels = this.outputService.getChannelDescriptors(); for (const channel of channels) { @@ -240,7 +259,7 @@ export class QuickAccessViewPickerAction extends Action2 { id: QuickAccessViewPickerAction.ID, title: { value: localize('quickOpenView', "Quick Open View"), original: 'Quick Open View' }, category: CATEGORIES.View, - f1: true, + f1: false, // hide quick pickers from command palette to not confuse with the other entry that shows a input field keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: undefined, diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 9d5057d1ab8..148c6c8d379 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -6,7 +6,7 @@ import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWindowsConfiguration } from 'vs/platform/window/common/window'; +import { IWindowsConfiguration, IWindowSettings } from 'vs/platform/window/common/window'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { localize } from 'vs/nls'; @@ -15,7 +15,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { RunOnceScheduler } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/resources'; -import { isMacintosh, isNative, isLinux } from 'vs/base/common/platform'; +import { isMacintosh, isNative, isLinux, isWindows } from 'vs/base/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -26,17 +26,21 @@ interface IConfiguration extends IWindowsConfiguration { debug?: { console?: { wordWrap?: boolean } }; editor?: { accessibilitySupport?: 'on' | 'off' | 'auto' }; security?: { workspace?: { trust?: { enabled?: boolean } } }; + window: IWindowSettings & { experimental?: { windowControlsOverlay?: { enabled?: boolean } } }; + workbench?: { experimental?: { settingsProfiles?: { enabled?: boolean } } }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { private titleBarStyle: 'native' | 'custom' | undefined; + private windowControlsOverlayEnabled: boolean | undefined; private nativeTabs: boolean | undefined; private nativeFullScreen: boolean | undefined; private clickThroughInactive: boolean | undefined; private updateMode: string | undefined; private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; private workspaceTrustEnabled: boolean | undefined; + private settingsProfilesEnabled: boolean | undefined; constructor( @IHostService private readonly hostService: IHostService, @@ -61,6 +65,13 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo changed = true; } + // Windows: Window Controls Overlay + if (isWindows && typeof config.window?.experimental?.windowControlsOverlay?.enabled === 'boolean' && config.window?.experimental?.windowControlsOverlay?.enabled !== this.windowControlsOverlayEnabled) { + this.windowControlsOverlayEnabled = config.window.experimental.windowControlsOverlay.enabled; + changed = true; + } + + // macOS: Native tabs if (isMacintosh && typeof config.window?.nativeTabs === 'boolean' && config.window.nativeTabs !== this.nativeTabs) { this.nativeTabs = config.window.nativeTabs; @@ -85,6 +96,12 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo changed = true; } + // Profiles + if (typeof config.workbench?.experimental?.settingsProfiles?.enabled === 'boolean' && config.workbench.experimental.settingsProfiles.enabled !== this.settingsProfilesEnabled) { + this.settingsProfilesEnabled = config.workbench.experimental.settingsProfiles.enabled; + changed = true; + } + // On linux turning on accessibility support will also pass this flag to the chrome renderer, thus a restart is required if (isLinux && typeof config.editor?.accessibilitySupport === 'string' && config.editor.accessibilitySupport !== this.accessibilitySupport) { this.accessibilitySupport = config.editor.accessibilitySupport; diff --git a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts index 16b16c8f366..5bc415b7516 100644 --- a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts +++ b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts @@ -46,7 +46,7 @@ export class SwitchRemoteViewItem extends SelectActionViewItem { const remoteAuthority = this.environmentService.remoteAuthority; isSetForConnection = true; const explorerType: string[] | undefined = remoteAuthority ? [remoteAuthority.split('+')[0]] : - this.storageService.get(REMOTE_EXPLORER_TYPE_KEY, StorageScope.WORKSPACE)?.split(',') ?? this.storageService.get(REMOTE_EXPLORER_TYPE_KEY, StorageScope.GLOBAL)?.split(','); + this.storageService.get(REMOTE_EXPLORER_TYPE_KEY, StorageScope.WORKSPACE)?.split(',') ?? this.storageService.get(REMOTE_EXPLORER_TYPE_KEY, StorageScope.PROFILE)?.split(','); if (explorerType !== undefined) { index = this.getOptionIndexForExplorerType(explorerType); } @@ -88,7 +88,7 @@ export class SwitchRemoteViewItem extends SelectActionViewItem { } static createOptionItems(views: IViewDescriptor[], contextKeyService: IContextKeyService): IRemoteSelectItem[] { - let options: IRemoteSelectItem[] = []; + const options: IRemoteSelectItem[] = []; views.forEach(view => { if (view.group && view.group.startsWith('targets') && view.remoteAuthority && (!view.when || contextKeyService.contextMatchesRules(view.when))) { options.push({ text: view.name, authority: isStringArray(view.remoteAuthority) ? view.remoteAuthority : [view.remoteAuthority] }); diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index f49b4d0acb6..e18a8624103 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -163,7 +163,7 @@ class HelpModel { remoteExplorerService: IRemoteExplorerService, environmentService: IWorkbenchEnvironmentService ) { - let helpItems: IHelpItem[] = []; + const helpItems: IHelpItem[] = []; const getStarted = viewModel.helpInformation.filter(info => info.getStarted); if (getStarted.length) { @@ -268,7 +268,7 @@ class HelpItemValue { private async getUrl(): Promise<string> { if (this._url === undefined) { if (this.urlOrCommand) { - let url = URI.parse(this.urlOrCommand); + const url = URI.parse(this.urlOrCommand); if (url.authority) { this._url = this.urlOrCommand; } else { @@ -305,9 +305,9 @@ abstract class HelpItemBase implements IHelpItem { if (remoteAuthority) { for (let i = 0; i < this.remoteExplorerService.targetType.length; i++) { if (remoteAuthority.startsWith(this.remoteExplorerService.targetType[i])) { - for (let value of this.values) { + for (const value of this.values) { if (value.remoteAuthority) { - for (let authority of value.remoteAuthority) { + for (const authority of value.remoteAuthority) { if (remoteAuthority.startsWith(authority)) { await this.takeAction(value.extensionDescription, await value.url); return; @@ -320,7 +320,7 @@ abstract class HelpItemBase implements IHelpItem { } if (this.values.length > 1) { - let actions = (await Promise.all(this.values.map(async (value) => { + const actions = (await Promise.all(this.values.map(async (value) => { return { label: value.extensionDescription.displayName || value.extensionDescription.identifier.value, description: await value.url, @@ -334,8 +334,9 @@ abstract class HelpItemBase implements IHelpItem { await this.takeAction(action.extensionDescription, action.description); } } + } else { + await this.takeAction(this.values[0].extensionDescription, await this.values[0].url); } - await this.takeAction(this.values[0].extensionDescription, await this.values[0].url); } @@ -479,8 +480,8 @@ export class RemoteViewPaneContainer extends FilterViewPaneContainer implements super(VIEWLET_ID, remoteExplorerService.onDidChangeTargetType, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, viewDescriptorService); this.addConstantViewDescriptors([this.helpPanelDescriptor]); remoteHelpExtPoint.setHandler((extensions) => { - let helpInformation: HelpInformation[] = []; - for (let extension of extensions) { + const helpInformation: HelpInformation[] = []; + for (const extension of extensions) { this._handleRemoteInfoExtensionPoint(extension, helpInformation); } diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 935ba605c07..10abb46d232 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -131,10 +131,9 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu } private get entry(): IStatusbarEntry { - let text: string; let tooltip: string; const count = this.remoteExplorerService.tunnelModel.forwarded.size + this.remoteExplorerService.tunnelModel.detected.size; - text = `${count}`; + const text = `${count}`; if (count === 0) { tooltip = nls.localize('remote.forwardedPorts.statusbarTextNone', "No Ports Forwarded"); } else { diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 954266d0fa3..91045b0fb95 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -306,7 +306,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } return; } - // show when in a virtual workspace + // Show when in a virtual workspace if (this.virtualWorkspaceLocation) { // Workspace with label: indicate editing source const workspaceLabel = this.labelService.getHostLabel(this.virtualWorkspaceLocation.scheme, this.virtualWorkspaceLocation.authority); @@ -330,8 +330,8 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr return; } } - // Remote actions: offer menu - if (this.getRemoteMenuActions().length > 0) { + // Show when there are commands other than the 'install additional remote extensions' command. + if (this.hasRemoteMenuCommands(true)) { this.renderRemoteStatusIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a Remote Window")); return; } @@ -343,7 +343,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr private renderRemoteStatusIndicator(text: string, tooltip?: string | IMarkdownString, command?: string, showProgress?: boolean): void { const name = nls.localize('remoteHost', "Remote Host"); - if (typeof command !== 'string' && this.getRemoteMenuActions().length > 0) { + if (typeof command !== 'string' && (this.hasRemoteMenuCommands(false))) { command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID; } @@ -403,9 +403,9 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr let lastCategoryName: string | undefined = undefined; - for (let actionGroup of actionGroups) { + for (const actionGroup of actionGroups) { let hasGroupCategory = false; - for (let action of actionGroup[1]) { + for (const action of actionGroup[1]) { if (action instanceof MenuItemAction) { if (!hasGroupCategory) { const category = getCategoryLabel(action); @@ -415,7 +415,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr } hasGroupCategory = true; } - let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; + const label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; items.push({ type: 'item', id: action.item.id, @@ -429,7 +429,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr type: 'separator' }); - let entriesBeforeConfig = items.length; + const entriesBeforeConfig = items.length; if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID) { if (this.remoteAuthority) { @@ -493,4 +493,15 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr quickPick.show(); } + + private hasRemoteMenuCommands(ignoreInstallAdditional: boolean): boolean { + if (this.remoteAuthority !== undefined || this.virtualWorkspaceLocation !== undefined) { + if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID) { + return true; + } + } else if (!ignoreInstallAdditional && this.extensionGalleryService.isEnabled()) { + return true; + } + return this.getRemoteMenuActions().length > 0; + } } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 17a8963a856..f3d20816324 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -468,7 +468,7 @@ class ActionBarRenderer extends Disposable implements ITableRenderer<ActionBarCe let actions: IAction[] = []; disposableStore.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions)); if (actions) { - let labelActions = actions.filter(action => action.id.toLowerCase().indexOf('label') >= 0); + const labelActions = actions.filter(action => action.id.toLowerCase().indexOf('label') >= 0); if (labelActions.length > 1) { labelActions.sort((a, b) => a.label.length - b.label.length); labelActions.pop(); diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 7da92791ee6..b6488780711 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -32,6 +32,7 @@ import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IDownloadService } from 'vs/platform/download/common/download'; import { DownloadServiceChannel } from 'vs/platform/download/common/downloadIpc'; import { timeout } from 'vs/base/common/async'; +import { TerminalLogConstants } from 'vs/platform/terminal/common/terminal'; export class LabelContribution implements IWorkbenchContribution { constructor( @@ -99,6 +100,7 @@ class RemoteLogOutputChannels implements IWorkbenchContribution { if (remoteEnv) { const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(OutputExt.OutputChannels); outputChannelRegistry.registerChannel({ id: 'remoteExtensionLog', label: localize('remoteExtensionLog', "Remote Server"), file: joinPath(remoteEnv.logsPath, `${RemoteExtensionLogFileName}.log`), log: true }); + outputChannelRegistry.registerChannel({ id: 'remotePtyHostLog', label: localize('remotePtyHostLog', "Remote Pty Host"), file: joinPath(remoteEnv.logsPath, `${TerminalLogConstants.FileName}.log`), log: true }); } }); } diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index a3db991bf05..efe65fa662e 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -173,6 +173,10 @@ background: transparent !important; } +.scm-view .monaco-list .monaco-list-row.cursor-default { + cursor: default; +} + .scm-view.show-actions .scm-provider > .actions, .scm-view.show-actions > .monaco-list .monaco-list-row .resource-group > .actions, .scm-view.show-actions > .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { @@ -241,6 +245,16 @@ margin: 0 0.2em 0 0; } +.scm-view .button-container > .monaco-button-dropdown { + flex-grow: 1; +} + +.scm-view .button-container > .monaco-button-dropdown > .monaco-dropdown-button { + display:flex; + align-items: center; + padding: 0 4px; +} + .scm-view .scm-editor.hidden { display: none; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a3059234eb1..1a733fb7680 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -20,7 +20,7 @@ import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from ' import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2 } from 'vs/platform/actions/common/actions'; -import { IAction, ActionRunner } from 'vs/base/common/actions'; +import { IAction, ActionRunner, Action, Separator } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, registerThemingParticipant, IFileIconTheme, ThemeIcon, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton } from './util'; @@ -82,10 +82,11 @@ import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/ import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { Button, ButtonWithDescription } from 'vs/base/browser/ui/button/button'; +import { Button, ButtonWithDescription, ButtonWithDropdown } from 'vs/base/browser/ui/button/button'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RepositoryContextKeys } from 'vs/workbench/contrib/scm/browser/scmViewService'; import { DropIntoEditorController } from 'vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource; @@ -107,8 +108,11 @@ class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton static readonly TEMPLATE_ID = 'actionButton'; get templateId(): string { return ActionButtonRenderer.TEMPLATE_ID; } + private actionButtons = new Map<ISCMActionButton, SCMActionButton>(); + constructor( @ICommandService private commandService: ICommandService, + @IContextMenuService private contextMenuService: IContextMenuService, @IThemeService private themeService: IThemeService, @INotificationService private notificationService: INotificationService, ) { } @@ -117,11 +121,11 @@ class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton // hack (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie'); - // Disable hover for list item - container.parentElement!.parentElement!.classList.add('force-no-hover'); + // Use default cursor & disable hover for list item + container.parentElement!.parentElement!.classList.add('cursor-default', 'force-no-hover'); const buttonContainer = append(container, $('.button-container')); - const actionButton = new SCMActionButton(buttonContainer, this.commandService, this.themeService, this.notificationService); + const actionButton = new SCMActionButton(buttonContainer, this.contextMenuService, this.commandService, this.themeService, this.notificationService); return { actionButton, disposable: Disposable.None, templateDisposable: actionButton }; } @@ -129,13 +133,25 @@ class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton renderElement(node: ITreeNode<ISCMActionButton, FuzzyScore>, index: number, templateData: ActionButtonTemplate, height: number | undefined): void { templateData.disposable.dispose(); + const disposables = new DisposableStore(); + const actionButton = node.element; templateData.actionButton.setButton(node.element.button); + + // Remember action button + this.actionButtons.set(actionButton, templateData.actionButton); + disposables.add({ dispose: () => this.actionButtons.delete(actionButton) }); + + templateData.disposable = disposables; } renderCompressedElements(): void { throw new Error('Should never happen since node is incompressible'); } + focusActionButton(actionButton: ISCMActionButton): void { + this.actionButtons.get(actionButton)?.focus(); + } + disposeElement(node: ITreeNode<ISCMActionButton, FuzzyScore>, index: number, template: ActionButtonTemplate): void { template.disposable.dispose(); } @@ -543,8 +559,8 @@ class ResourceRenderer implements ICompressibleTreeRenderer<ISCMResource | IReso } // FilePath match - let labelMatches: IMatch[] = []; - let descriptionMatches: IMatch[] = []; + const labelMatches: IMatch[] = []; + const descriptionMatches: IMatch[] = []; for (const match of matches) { if (match.start > pathLength) { @@ -1164,7 +1180,7 @@ class ViewModel { configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); this.onDidChangeConfiguration(); - Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element)) + Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables) (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); this.disposables.add(this.tree.onDidChangeCollapseState(() => this._treeViewStateIsStale = true)); @@ -1710,7 +1726,8 @@ class ExpandAllRepositoriesAction extends ViewAction<SCMViewPane> { registerAction2(CollapseAllRepositoriesAction); registerAction2(ExpandAllRepositoriesAction); -class SCMInputWidget extends Disposable { +class SCMInputWidget { + private static readonly ValidationTimeouts: { [severity: number]: number } = { [InputValidationType.Information]: 5000, [InputValidationType.Warning]: 8000, @@ -1723,6 +1740,7 @@ class SCMInputWidget extends Disposable { private editorContainer: HTMLElement; private placeholderTextContainer: HTMLElement; private inputEditor: CodeEditorWidget; + private disposables = new DisposableStore(); private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; private repositoryIdContextKey: IContextKey<string | undefined>; @@ -1798,7 +1816,7 @@ class SCMInputWidget extends Disposable { // Adaptive indentation rules const opts = this.modelService.getCreationOptions(textModel.getLanguageId(), textModel.uri, textModel.isForSimpleWidget); - const onEnter = Event.filter(this.inputEditor.onKeyDown, e => e.keyCode === KeyCode.Enter); + const onEnter = Event.filter(this.inputEditor.onKeyDown, e => e.keyCode === KeyCode.Enter, this.repositoryDisposables); this.repositoryDisposables.add(onEnter(() => textModel.detectIndentation(opts.insertSpaces, opts.tabSize))); // Keep model in sync with API @@ -1862,6 +1880,13 @@ class SCMInputWidget extends Disposable { this.repositoryDisposables.add(input.repository.provider.onDidChangeCommitTemplate(updateTemplate, this)); updateTemplate(); + // Update input enablement + const updateEnablement = (enabled: boolean) => { + this.inputEditor.updateOptions({ readOnly: !enabled }); + }; + this.repositoryDisposables.add(input.onDidChangeEnablement(enabled => updateEnablement(enabled))); + updateEnablement(input.enabled); + // Save model this.model = { input, textModel }; } @@ -1907,8 +1932,6 @@ class SCMInputWidget extends Disposable { @IContextViewService private readonly contextViewService: IContextViewService, @IOpenerService private readonly openerService: IOpenerService, ) { - super(); - this.element = append(container, $('.scm-editor')); this.editorContainer = append(this.element, $('.scm-editor-container')); this.placeholderTextContainer = append(this.editorContainer, $('.scm-editor-placeholder')); @@ -1916,6 +1939,9 @@ class SCMInputWidget extends Disposable { const fontFamily = this.getInputEditorFontFamily(); const fontSize = this.getInputEditorFontSize(); const lineHeight = this.computeLineHeight(fontSize); + // We respect the configured `editor.accessibilitySupport` setting to be able to have wrapping + // even when a screen reader is attached. + const accessibilitySupport = this.configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'); this.setPlaceholderFontStyles(fontFamily, fontSize, lineHeight); @@ -1938,29 +1964,31 @@ class SCMInputWidget extends Disposable { overflowWidgetsDomNode, renderWhitespace: 'none', enableDropIntoEditor: true, + accessibilitySupport }; const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { isSimpleWidget: true, contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - SuggestController.ID, - SnippetController2.ID, - MenuPreventer.ID, - SelectionClipboardContributionID, - ContextMenuController.ID, ColorDetector.ID, - ModesHoverController.ID, + ContextMenuController.ID, + DropIntoEditorController.ID, LinkDetector.ID, - DropIntoEditorController.ID + MenuPreventer.ID, + MessageController.ID, + ModesHoverController.ID, + SelectionClipboardContributionID, + SnippetController2.ID, + SuggestController.ID, ]) }; const services = new ServiceCollection([IContextKeyService, contextKeyService2]); const instantiationService2 = instantiationService.createChild(services); this.inputEditor = instantiationService2.createInstance(CodeEditorWidget, this.editorContainer, editorOptions, codeEditorWidgetOptions); - this._register(this.inputEditor); + this.disposables.add(this.inputEditor); - this._register(this.inputEditor.onDidFocusEditorText(() => { + this.disposables.add(this.inputEditor.onDidFocusEditorText(() => { if (this.input?.repository) { this.scmViewService.focus(this.input.repository); } @@ -1968,7 +1996,7 @@ class SCMInputWidget extends Disposable { this.editorContainer.classList.add('synthetic-focus'); this.renderValidation(); })); - this._register(this.inputEditor.onDidBlurEditorText(() => { + this.disposables.add(this.inputEditor.onDidBlurEditorText(() => { this.editorContainer.classList.remove('synthetic-focus'); setTimeout(() => { @@ -1981,7 +2009,7 @@ class SCMInputWidget extends Disposable { const firstLineKey = contextKeyService2.createKey<boolean>('scmInputIsInFirstPosition', false); const lastLineKey = contextKeyService2.createKey<boolean>('scmInputIsInLastPosition', false); - this._register(this.inputEditor.onDidChangeCursorPosition(({ position }) => { + this.disposables.add(this.inputEditor.onDidChangeCursorPosition(({ position }) => { const viewModel = this.inputEditor._getViewModel()!; const lastLineNumber = viewModel.getLineCount(); const lastLineCol = viewModel.getLineContent(lastLineNumber).length + 1; @@ -1990,22 +2018,42 @@ class SCMInputWidget extends Disposable { lastLineKey.set(viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol); })); - const onInputFontFamilyChanged = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.inputFontFamily') || e.affectsConfiguration('scm.inputFontSize')); - this._register(onInputFontFamilyChanged(() => { + const relevantSettings = [ + 'scm.inputFontFamily', + 'editor.fontFamily', // When `scm.inputFontFamily` is 'editor', we use it as an effective value + 'scm.inputFontSize', + 'editor.accessibilitySupport' + ]; + + const onInputFontFamilyChanged = Event.filter( + this.configurationService.onDidChangeConfiguration, + (e) => { + for (const setting of relevantSettings) { + if (e.affectsConfiguration(setting)) { + return true; + } + } + return false; + }, + this.disposables + ); + this.disposables.add(onInputFontFamilyChanged(() => { const fontFamily = this.getInputEditorFontFamily(); const fontSize = this.getInputEditorFontSize(); const lineHeight = this.computeLineHeight(fontSize); + const accessibilitySupport = this.configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'); this.inputEditor.updateOptions({ fontFamily: fontFamily, fontSize: fontSize, lineHeight: lineHeight, + accessibilitySupport }); this.setPlaceholderFontStyles(fontFamily, fontSize, lineHeight); })); - this.onDidChangeContentHeight = Event.signal(Event.filter(this.inputEditor.onDidContentSizeChange, e => e.contentHeightChanged)); + this.onDidChangeContentHeight = Event.signal(Event.filter(this.inputEditor.onDidContentSizeChange, e => e.contentHeightChanged, this.disposables)); } getContentHeight(): number { @@ -2135,11 +2183,11 @@ class SCMInputWidget extends Disposable { this.validationHasFocus = false; } - override dispose(): void { + dispose(): void { this.input = undefined; this.repositoryDisposables.dispose(); this.clearValidation(); - super.dispose(); + this.disposables.dispose(); } } @@ -2166,6 +2214,8 @@ export class SCMViewPane extends ViewPane { get viewModel(): ViewModel { return this._viewModel; } private listLabels!: ResourceLabels; private inputRenderer!: InputRenderer; + private actionButtonRenderer!: ActionButtonRenderer; + private readonly disposables = new DisposableStore(); constructor( options: IViewPaneOptions, @@ -2205,7 +2255,7 @@ export class SCMViewPane extends ViewPane { const overflowWidgetsDomNode = $('.scm-overflow-widgets-container.monaco-editor'); const updateActionsVisibility = () => this.listContainer.classList.toggle('show-actions', this.configurationService.getValue<boolean>('scm.alwaysShowActions')); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'))(updateActionsVisibility)); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'), this.disposables)(updateActionsVisibility)); updateActionsVisibility(); const updateProviderCountVisibility = () => { @@ -2213,12 +2263,14 @@ export class SCMViewPane extends ViewPane { this.listContainer.classList.toggle('hide-provider-counts', value === 'hidden'); this.listContainer.classList.toggle('auto-provider-counts', value === 'auto'); }; - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'))(updateProviderCountVisibility)); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility)); updateProviderCountVisibility(); this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => this.tree.updateElementHeight(input, height)); const delegate = new ListDelegate(this.inputRenderer); + this.actionButtonRenderer = this.instantiationService.createInstance(ActionButtonRenderer); + this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this._register(this.listLabels); @@ -2229,7 +2281,7 @@ export class SCMViewPane extends ViewPane { const renderers: ICompressibleTreeRenderer<any, any, any>[] = [ this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), this.inputRenderer, - this.instantiationService.createInstance(ActionButtonRenderer), + this.actionButtonRenderer, this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), this._register(this.instantiationService.createInstance(ResourceRenderer, () => this._viewModel, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner)) ]; @@ -2281,7 +2333,7 @@ export class SCMViewPane extends ViewPane { this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'))(this.updateActions, this)); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.disposables)(this.updateActions, this)); this.updateActions(); } @@ -2359,6 +2411,7 @@ export class SCMViewPane extends ViewPane { return; } else if (isSCMActionButton(e.element)) { this.scmViewService.focus(e.element.repository); + this.actionButtonRenderer.focusActionButton(e.element); return; } @@ -2372,9 +2425,7 @@ export class SCMViewPane extends ViewPane { if (e.editorOptions.pinned) { const activeEditorPane = this.editorService.activeEditorPane; - if (activeEditorPane) { - activeEditorPane.group.pinEditor(activeEditorPane.input); - } + activeEditorPane?.group.pinEditor(activeEditorPane.input); } } @@ -2460,6 +2511,11 @@ export class SCMViewPane extends ViewPane { override getActionsContext(): unknown { return this.scmViewService.visibleRepositories.length === 1 ? this.scmViewService.visibleRepositories[0].provider : undefined; } + + override dispose(): void { + this.disposables.dispose(); + super.dispose(); + } } export const scmProviderSeparatorBorderColor = registerColor('scm.providerBorder', { dark: '#454545', light: '#C8C8C8', hcDark: contrastBorder, hcLight: contrastBorder }, localize('scm.providerBorder', "SCM Provider separator border.")); @@ -2558,11 +2614,12 @@ registerThemingParticipant((theme, collector) => { }); export class SCMActionButton implements IDisposable { - private button: Button | ButtonWithDescription | undefined; + private button: Button | ButtonWithDescription | ButtonWithDropdown | undefined; private readonly disposables = new MutableDisposable<DisposableStore>(); constructor( private readonly container: HTMLElement, + private readonly contextMenuService: IContextMenuService, private readonly commandService: ICommandService, private readonly themeService: IThemeService, private readonly notificationService: INotificationService @@ -2580,28 +2637,54 @@ export class SCMActionButton implements IDisposable { return; } - if (button.description) { + const executeButtonAction = async (commandId: string, ...args: any[]) => { + try { + await this.commandService.executeCommand(commandId, ...args); + } catch (ex) { + this.notificationService.error(ex); + } + }; + + if (button.secondaryCommands?.length) { + const actions: IAction[] = []; + for (let index = 0; index < button.secondaryCommands.length; index++) { + for (const command of button.secondaryCommands[index]) { + actions.push(new Action(command.id, command.title, undefined, true, async () => await executeButtonAction(command.id, ...(command.arguments || [])))); + } + if (index !== button.secondaryCommands.length - 1) { + actions.push(new Separator()); + } + } + + // ButtonWithDropdown + this.button = new ButtonWithDropdown(this.container, { + actions: actions, + addPrimaryActionToDropdown: false, + contextMenuProvider: this.contextMenuService, + supportIcons: true + }); + } else if (button.description) { // ButtonWithDescription this.button = new ButtonWithDescription(this.container, { supportIcons: true, title: button.command.tooltip }); (this.button as ButtonWithDescription).description = button.description; } else { // Button - this.button = new Button(this.container, { supportIcons: true }); + this.button = new Button(this.container, { supportIcons: true, title: button.command.tooltip }); } + this.button.enabled = button.enabled; this.button.label = button.command.title; - this.button.onDidClick(async () => { - try { - await this.commandService.executeCommand(button.command.id, ...(button.command.arguments || [])); - } catch (ex) { - this.notificationService.error(ex); - } - }, null, this.disposables.value); + this.button.element.title = button.command.tooltip ?? ''; + this.button.onDidClick(async () => await executeButtonAction(button.command.id, ...(button.command.arguments || [])), null, this.disposables.value); this.disposables.value!.add(this.button); this.disposables.value!.add(attachButtonStyler(this.button, this.themeService)); } + focus(): void { + this.button?.focus(); + } + private clear(): void { this.disposables.value = new DisposableStore(); this.button = undefined; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index be0176c4919..242793d6c48 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -143,7 +143,7 @@ export class SCMViewService implements ISCMViewService { } return { added, removed }; - }, 0) + }, 0, undefined, undefined, this.disposables) ); get focusedRepository(): ISCMRepository | undefined { diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 80d5d19302d..c8f4524d017 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -37,7 +37,7 @@ export function isSCMResource(element: any): element is ISCMResource { return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup); } -const compareActions = (a: IAction, b: IAction) => a.id === b.id; +const compareActions = (a: IAction, b: IAction) => a.id === b.id && a.enabled === b.enabled; export function connectPrimaryMenu(menu: IMenu, callback: (primary: IAction[], secondary: IAction[]) => void, primaryGroup?: string): IDisposable { let cachedDisposable: IDisposable = Disposable.None; diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index e180e8e9d3c..0629e491de5 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -99,7 +99,9 @@ export interface ISCMInputChangeEvent { export interface ISCMActionButtonDescriptor { command: Command; + secondaryCommands?: Command[][]; description?: string; + enabled: boolean; } export interface ISCMActionButton { @@ -121,6 +123,9 @@ export interface ISCMInput { validateInput: IInputValidator; readonly onDidChangeValidateInput: Event<void>; + enabled: boolean; + readonly onDidChangeEnablement: Event<boolean>; + visible: boolean; readonly onDidChangeVisibility: Event<boolean>; diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 7877ba1d34e..9231a8ac8b8 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -38,6 +38,20 @@ class SCMInput implements ISCMInput { private readonly _onDidChangePlaceholder = new Emitter<string>(); readonly onDidChangePlaceholder: Event<string> = this._onDidChangePlaceholder.event; + private _enabled = true; + + get enabled(): boolean { + return this._enabled; + } + + set enabled(enabled: boolean) { + this._enabled = enabled; + this._onDidChangeEnablement.fire(enabled); + } + + private readonly _onDidChangeEnablement = new Emitter<boolean>(); + readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event; + private _visible = true; get visible(): boolean { @@ -90,45 +104,45 @@ class SCMInput implements ISCMInput { } // Migrate from old format // TODO@joao: remove this migration code a few releases - const userKeys = Iterable.filter(Iterable.from(storageService.keys(StorageScope.GLOBAL, StorageTarget.USER)), key => key.startsWith('scm/input:')); + const userKeys = Iterable.filter(Iterable.from(storageService.keys(StorageScope.APPLICATION, StorageTarget.USER)), key => key.startsWith('scm/input:')); for (const key of userKeys) { try { - const rawHistory = storageService.get(key, StorageScope.GLOBAL, ''); + const rawHistory = storageService.get(key, StorageScope.APPLICATION, ''); const history = JSON.parse(rawHistory); if (Array.isArray(history)) { if (history.length === 0 || (history.length === 1 && history[0] === '')) { // remove empty histories - storageService.remove(key, StorageScope.GLOBAL); + storageService.remove(key, StorageScope.APPLICATION); } else { // migrate existing histories to have a timestamp - storageService.store(key, JSON.stringify({ timestamp: new Date().getTime(), history }), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(key, JSON.stringify({ timestamp: new Date().getTime(), history }), StorageScope.APPLICATION, StorageTarget.MACHINE); } } else { // move to MACHINE target - storageService.store(key, rawHistory, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(key, rawHistory, StorageScope.APPLICATION, StorageTarget.MACHINE); } } catch { // remove unparseable entries - storageService.remove(key, StorageScope.GLOBAL); + storageService.remove(key, StorageScope.APPLICATION); } } // Garbage collect - const machineKeys = Iterable.filter(Iterable.from(storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE)), key => key.startsWith('scm/input:')); + const machineKeys = Iterable.filter(Iterable.from(storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE)), key => key.startsWith('scm/input:')); for (const key of machineKeys) { try { - const history = JSON.parse(storageService.get(key, StorageScope.GLOBAL, '')); + const history = JSON.parse(storageService.get(key, StorageScope.APPLICATION, '')); if (Array.isArray(history?.history) && Number.isInteger(history?.timestamp) && new Date().getTime() - history?.timestamp > 2592000000) { // garbage collect after 30 days - storageService.remove(key, StorageScope.GLOBAL); + storageService.remove(key, StorageScope.APPLICATION); } } catch { // remove unparseable entries - storageService.remove(key, StorageScope.GLOBAL); + storageService.remove(key, StorageScope.APPLICATION); } } @@ -146,7 +160,7 @@ class SCMInput implements ISCMInput { if (key) { try { - history = JSON.parse(this.storageService.get(key, StorageScope.GLOBAL, '')).history; + history = JSON.parse(this.storageService.get(key, StorageScope.APPLICATION, '')).history; history = history?.map(s => s ?? ''); } catch { // noop @@ -175,9 +189,9 @@ class SCMInput implements ISCMInput { const history = [...this.historyNavigator].map(s => s ?? ''); if (history.length === 0 || (history.length === 1 && history[0] === '')) { - storageService.remove(key, StorageScope.GLOBAL); + storageService.remove(key, StorageScope.APPLICATION); } else { - storageService.store(key, JSON.stringify({ timestamp: new Date().getTime(), history }), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(key, JSON.stringify({ timestamp: new Date().getTime(), history }), StorageScope.APPLICATION, StorageTarget.MACHINE); } this.didChangeHistory = false; }); diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index f9e5c40fa0d..f59811de9a2 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -47,8 +47,7 @@ .search-view .search-widget .monaco-inputbox > .ibwrapper > .mirror, .search-view .search-widget .monaco-inputbox > .ibwrapper > textarea.input { - padding: 3px; - padding-left: 4px; + padding: 3px 0px 3px 4px; } /* NOTE: height is also used in searchWidget.ts as a constant*/ diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 2a79eacad3b..77782e09059 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -55,7 +55,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ISearchConfiguration, SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, VIEW_ID } from 'vs/workbench/services/search/common/search'; -import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configurationMigration'; +import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); registerSingleton(ISearchHistoryService, SearchHistoryService, true); @@ -594,7 +594,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated()) }); -registerAction2(class ShowAllSymbolsAction extends Action2 { +class ShowAllSymbolsAction extends Action2 { static readonly ID = 'workbench.action.showAllSymbols'; static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace..."); @@ -619,7 +619,9 @@ registerAction2(class ShowAllSymbolsAction extends Action2 { override async run(accessor: ServicesAccessor): Promise<void> { accessor.get(IQuickInputService).quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); } -}); +} + +registerAction2(ShowAllSymbolsAction); const SEARCH_MODE_CONFIG = 'search.mode'; @@ -791,7 +793,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: AnythingQuickAccessProvider.PREFIX, placeholder: nls.localize('anythingQuickAccessPlaceholder', "Search files by name (append {0} to go to line or {1} to go to symbol)", AbstractGotoLineQuickAccessProvider.PREFIX, GotoSymbolQuickAccessProvider.PREFIX), contextKey: defaultQuickAccessContextKeyValue, - helpEntries: [{ description: nls.localize('anythingQuickAccess', "Go to File"), needsEditor: false }] + helpEntries: [{ description: nls.localize('anythingQuickAccess', "Go to File"), commandId: 'workbench.action.quickOpen' }] }); quickAccessRegistry.registerQuickAccessProvider({ @@ -799,7 +801,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: SymbolsQuickAccessProvider.PREFIX, placeholder: nls.localize('symbolsQuickAccessPlaceholder', "Type the name of a symbol to open."), contextKey: 'inWorkspaceSymbolsPicker', - helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), needsEditor: false }] + helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), commandId: ShowAllSymbolsAction.ID }] }); // Configuration diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index becdd37f551..e6be26f64a1 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -291,9 +291,7 @@ export function cancelSearch(accessor: ServicesAccessor) { export function refreshSearch(accessor: ServicesAccessor) { const viewsService = accessor.get(IViewsService); const searchView = getSearchView(viewsService); - if (searchView) { - searchView.triggerQueryChange({ preserveFocus: false }); - } + searchView?.triggerQueryChange({ preserveFocus: false }); } export function collapseDeepestExpandedLevel(accessor: ServicesAccessor) { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 17baf536043..8f87fe9247d 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -395,9 +395,7 @@ export class SearchView extends ViewPane { } // Enable highlights if there are searchresults - if (this.viewModel) { - this.viewModel.searchResult.toggleHighlights(visible); - } + this.viewModel?.searchResult.toggleHighlights(visible); } get searchAndReplaceWidget(): SearchWidget { @@ -483,18 +481,14 @@ export class SearchView extends ViewPane { this._register(inputFocusTracker.onDidFocus(() => { this.lastFocusState = 'input'; this.inputBoxFocused.set(true); - if (contextKey) { - contextKey.set(true); - } + contextKey?.set(true); })); this._register(inputFocusTracker.onDidBlur(() => { this.inputBoxFocused.set(this.searchWidget.searchInputHasFocus() || this.searchWidget.replaceInputHasFocus() || this.inputPatternIncludes.inputHasFocus() || this.inputPatternExcludes.inputHasFocus()); - if (contextKey) { - contextKey.set(false); - } + contextKey?.set(false); })); } @@ -752,9 +746,7 @@ export class SearchView extends ViewPane { this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, 75, true)(options => { if (options.element instanceof Match) { const selectedMatch: Match = options.element; - if (this.currentSelectedFileMatch) { - this.currentSelectedFileMatch.setSelectedMatch(null); - } + this.currentSelectedFileMatch?.setSelectedMatch(null); this.currentSelectedFileMatch = selectedMatch.parent(); this.currentSelectedFileMatch.setSelectedMatch(selectedMatch); diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index 49d1e61bf03..faed6ff43bd 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -75,7 +75,7 @@ export async function getWorkspaceSymbols(query: string, token: CancellationToke if (!value) { return; } - for (let symbol of value) { + for (const symbol of value) { all.push(new WorkspaceSymbolItem(symbol, provider)); } } catch (err) { diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index 578221a80ef..4df55f37483 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -5,33 +5,32 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { compareFileExtensions, compareFileNames, comparePaths } from 'vs/base/common/comparers'; +import { memoize } from 'vs/base/common/decorators'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { getBaseLabel } from 'vs/base/common/labels'; -import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap, TernarySearchTree } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; import { lcut } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; -import { FindMatch, IModelDeltaDecoration, ITextModel, OverviewRulerLane, TrackedRangeStickiness, MinimapPosition } from 'vs/editor/common/model'; +import { FindMatch, IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelService } from 'vs/editor/common/services/model'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileService, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; -import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchConfigurationProperties, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchMatch, ITextSearchStats, resultIsMatch, ISearchRange, OneLineRange, ITextSearchContext, ITextSearchResult, SearchSortOrder, SearchCompletionExitCode } from 'vs/workbench/services/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { overviewRulerFindMatchForeground, minimapFindMatch } from 'vs/platform/theme/common/colorRegistry'; +import { minimapFindMatch, overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; -import { editorMatchesToTextSearchResults, addContextToEditorMatches } from 'vs/workbench/services/search/common/searchHelpers'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import { memoize } from 'vs/base/common/decorators'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { compareFileNames, compareFileExtensions, comparePaths } from 'vs/base/common/comparers'; -import { IFileService, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files'; -import { Schemas } from 'vs/base/common/network'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; +import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { addContextToEditorMatches, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; export class Match { @@ -217,8 +216,15 @@ export class FileMatch extends Disposable implements IFileMatch { return new Map(this._context); } - constructor(private _query: IPatternInfo, private _previewOptions: ITextSearchPreviewOptions | undefined, private _maxResults: number | undefined, private _parent: FolderMatch, private rawMatch: IFileMatch, - @IModelService private readonly modelService: IModelService, @IReplaceService private readonly replaceService: IReplaceService + constructor( + private _query: IPatternInfo, + private _previewOptions: ITextSearchPreviewOptions | undefined, + private _maxResults: number | undefined, + private _parent: FolderMatch, + private rawMatch: IFileMatch, + @IModelService private readonly modelService: IModelService, + @IReplaceService private readonly replaceService: IReplaceService, + @ILabelService private readonly labelService: ILabelService, ) { super(); this._resource = this.rawMatch.resource; @@ -407,7 +413,7 @@ export class FileMatch extends Disposable implements IFileMatch { } name(): string { - return getBaseLabel(this.resource); + return this.labelService.getUriBasenameLabel(this.resource); } addContext(results: ITextSearchResult[] | undefined) { @@ -472,9 +478,16 @@ export class FolderMatch extends Disposable { private _unDisposedFileMatches: ResourceMap<FileMatch>; private _replacingAll: boolean = false; - constructor(protected _resource: URI | null, private _id: string, private _index: number, private _query: ITextQuery, private _parent: SearchResult, private _searchModel: SearchModel, + constructor( + protected _resource: URI | null, + private _id: string, + private _index: number, + private _query: ITextQuery, + private _parent: SearchResult, + private _searchModel: SearchModel, @IReplaceService private readonly replaceService: IReplaceService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILabelService private readonly labelService: ILabelService, ) { super(); this._fileMatches = new ResourceMap<FileMatch>(); @@ -506,7 +519,7 @@ export class FolderMatch extends Disposable { } name(): string { - return getBaseLabel(withNullAsUndefined(this.resource)) || ''; + return this.resource ? this.labelService.getUriBasenameLabel(this.resource) : ''; } parent(): SearchResult { @@ -515,9 +528,7 @@ export class FolderMatch extends Disposable { bindModel(model: ITextModel): void { const fileMatch = this._fileMatches.get(model.uri); - if (fileMatch) { - fileMatch.bindModel(model); - } + fileMatch?.bindModel(model); } add(raw: IFileMatch[], silent: boolean): void { @@ -651,9 +662,10 @@ export class FolderMatch extends Disposable { export class FolderMatchWithResource extends FolderMatch { constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult, _searchModel: SearchModel, @IReplaceService replaceService: IReplaceService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService ) { - super(_resource, _id, _index, _query, _parent, _searchModel, replaceService, instantiationService); + super(_resource, _id, _index, _query, _parent, _searchModel, replaceService, instantiationService, labelService); } override get resource(): URI { @@ -770,9 +782,7 @@ export class SearchResult extends Disposable { private onModelAdded(model: ITextModel): void { const folderMatch = this._folderMatchesMap.findSubstr(model.uri); - if (folderMatch) { - folderMatch.bindModel(model); - } + folderMatch?.bindModel(model); } private createFolderMatchWithResource(resource: URI, id: string, index: number, query: ITextQuery): FolderMatchWithResource { @@ -804,9 +814,7 @@ export class SearchResult extends Disposable { } const folderMatch = this.getFolderMatch(raw[0].resource); - if (folderMatch) { - folderMatch.add(raw, silent); - } + folderMatch?.add(raw, silent); }); this._otherFilesMatch?.add(other, silent); @@ -1063,9 +1071,7 @@ export class SearchModel extends Disposable { progressEmitter.fire(); this.onSearchProgress(p); - if (onProgress) { - onProgress(p); - } + onProgress?.(p); }); const dispose = () => tokenSource.dispose(); @@ -1076,6 +1082,7 @@ export class SearchModel extends Disposable { Promise.race([currentRequest, Event.toPromise(progressEmitter.event)]).finally(() => { /* __GDPR__ "searchResultsFirstRender" : { + "owner": "roblourens", "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } } */ @@ -1091,6 +1098,7 @@ export class SearchModel extends Disposable { } finally { /* __GDPR__ "searchResultsFinished" : { + "owner": "roblourens", "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } } */ @@ -1119,6 +1127,7 @@ export class SearchModel extends Disposable { /* __GDPR__ "searchResultsShown" : { + "owner": "roblourens", "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "fileCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "options": { "${inline}": [ "${IPatternInfo}" ] }, diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index ddc474564ac..b0517c6a59f 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -3,26 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { isWindows } from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IModelService } from 'vs/editor/common/services/model'; import { ModelService } from 'vs/editor/common/services/modelService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { FileService } from 'vs/platform/files/common/fileService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { IFileMatch, ITextSearchMatch, OneLineRange, QueryType, SearchSortOrder } from 'vs/workbench/services/search/common/search'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; -import { isWindows } from 'vs/base/common/platform'; -import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { NullLogService } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { NullLogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { MockLabelService } from 'vs/workbench/services/label/test/common/mockLabelService'; +import { IFileMatch, ITextSearchMatch, OneLineRange, QueryType, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Search - Viewlet', () => { let instantiation: TestInstantiationService; @@ -33,6 +35,7 @@ suite('Search - Viewlet', () => { instantiation.stub(IModelService, stubModelService(instantiation)); instantiation.set(IWorkspaceContextService, new TestContextService(TestWorkspace)); instantiation.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); + instantiation.stub(ILabelService, new MockLabelService()); }); test('Data Source', function () { diff --git a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts index 104eb095df6..cd49e55a241 100644 --- a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts @@ -113,9 +113,7 @@ suite('SearchModel', () => { function canceleableSearchService(tokenSource: CancellationTokenSource): ISearchService { return <ISearchService>{ textSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete> { - if (token) { - token.onCancellationRequested(() => tokenSource.cancel()); - } + token?.onCancellationRequested(() => tokenSource.cancel()); return new Promise(resolve => { queueMicrotask(() => { diff --git a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts b/src/vs/workbench/contrib/search/test/common/searchResult.test.ts index 5e61596725e..651a67805bd 100644 --- a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchResult.test.ts @@ -22,6 +22,8 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { MockLabelService } from 'vs/workbench/services/label/test/common/mockLabelService'; const lineOneRange = new OneLineRange(1, 0, 1); @@ -36,6 +38,7 @@ suite('SearchResult', () => { instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); instantiationService.stubPromise(IReplaceService, {}); instantiationService.stubPromise(IReplaceService, 'replace', null); + instantiationService.stub(ILabelService, new MockLabelService()); }); test('Line Match', function () { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 34e99b16ad4..380f8dbcc8f 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -13,12 +13,11 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/searchEditor'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { ICodeEditorViewState, IEditor } from 'vs/editor/common/editorCommon'; -import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { ICodeEditorViewState } from 'vs/editor/common/editorCommon'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { ReferencesController } from 'vs/editor/contrib/gotoSymbol/browser/peek/referencesController'; @@ -37,7 +36,7 @@ import { inputBorder, registerColor, searchEditorFindMatch, searchEditorFindMatc import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { AbstractTextCodeEditor } from 'vs/workbench/browser/parts/editor/textCodeEditor'; import { EditorInputCapabilities, IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; @@ -68,13 +67,13 @@ const FILE_LINE_REGEX = /^(\S.*):$/; type SearchEditorViewState = ICodeEditorViewState & { focused: 'input' | 'editor' }; -export class SearchEditor extends BaseTextEditor<SearchEditorViewState> { +export class SearchEditor extends AbstractTextCodeEditor<SearchEditorViewState> { static readonly ID: string = SearchEditorID; static readonly SEARCH_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'searchEditorViewState'; private queryEditorWidget!: SearchWidget; - private searchResultEditor!: CodeEditorWidget; + private get searchResultEditor() { return this.editorControl!; } private queryEditorContainer!: HTMLElement; private dimension?: DOM.Dimension; private inputPatternIncludes!: IncludePatternInputWidget; @@ -112,9 +111,9 @@ export class SearchEditor extends BaseTextEditor<SearchEditorViewState> { @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, @IConfigurationService protected configurationService: IConfigurationService, - @IFileService private readonly fileService: IFileService + @IFileService fileService: IFileService ) { - super(SearchEditor.ID, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService); + super(SearchEditor.ID, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); this.container = DOM.$('.search-editor'); this.searchOperation = this._register(new LongRunningOperation(progressService)); @@ -231,12 +230,11 @@ export class SearchEditor extends BaseTextEditor<SearchEditorViewState> { return EditorExtensionsRegistry.getEditorContributions().filter(c => skipContributions.indexOf(c.id) === -1); } - protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IEditor { - return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, { contributions: this._getContributions() }); + protected override getCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { + return { contributions: this._getContributions() }; } private registerEditorListeners() { - this.searchResultEditor = super.getControl() as CodeEditorWidget; this.searchResultEditor.onMouseUp(e => { if (e.event.detail === 2) { const behaviour = this.searchConfig.searchEditor.doubleClickBehaviour; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 28e8b4fd1e5..8878b684466 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -139,7 +139,7 @@ export const openNewSearchEditor = } } - telemetryService.publicLog2('searchEditor/openNewSearchEditor'); + telemetryService.publicLog2<{}, { owner: 'roblourens' }>('searchEditor/openNewSearchEditor'); const seedSearchStringFromSelection = _args.location === 'new' || configurationService.getValue<IEditorOptions>('editor').find!.seedSearchStringFromSelection; const args: OpenSearchEditorArgs = { query: seedSearchStringFromSelection ? selected : undefined }; @@ -189,7 +189,7 @@ export const createEditorFromSearchResult = const sortOrder = configurationService.getValue<ISearchConfigurationProperties>('search').sortOrder; - telemetryService.publicLog2('searchEditor/createEditorFromSearchResult'); + telemetryService.publicLog2<{}, { owner: 'roblourens' }>('searchEditor/createEditorFromSearchResult'); const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index dfd56c33267..56f822657f5 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -184,7 +184,7 @@ export class SearchEditorInput extends EditorInput { override async saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<EditorInput | undefined> { const path = await this.fileDialogService.pickFileToSave(await this.suggestFileName(), options?.availableFileSystems); if (path) { - this.telemetryService.publicLog2('searchEditor/saveSearchResults'); + this.telemetryService.publicLog2<{}, { owner: 'roblourens' }>('searchEditor/saveSearchResults'); const toWrite = await this.serializeForDisk(); if (await this.textFileService.create([{ resource: path, value: toWrite, options: { overwrite: true } }])) { this.setDirty(false); diff --git a/src/vs/workbench/contrib/sessionSync/browser/sessionSync.contribution.ts b/src/vs/workbench/contrib/sessionSync/browser/sessionSync.contribution.ts new file mode 100644 index 00000000000..d075977cb45 --- /dev/null +++ b/src/vs/workbench/contrib/sessionSync/browser/sessionSync.contribution.ts @@ -0,0 +1,528 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize } from 'vs/nls'; +import { ISessionSyncWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_TITLE, EditSessionSchemaVersion } from 'vs/workbench/services/sessionSync/common/sessionSync'; +import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { URI } from 'vs/base/common/uri'; +import { joinPath, relativePath } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { SessionSyncWorkbenchService } from 'vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtualWorkspace'; +import { Schemas } from 'vs/base/common/network'; +import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; +import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; + +registerSingleton(ISessionSyncWorkbenchService, SessionSyncWorkbenchService); + +const resumeLatestCommand = { + id: 'workbench.experimental.editSessions.actions.resumeLatest', + title: { value: localize('resume latest', "{0}: Resume Latest Edit Session", EDIT_SESSION_SYNC_TITLE), original: 'Edit Sessions' }, +}; +const storeCurrentCommand = { + id: 'workbench.experimental.editSessions.actions.storeCurrent', + title: { value: localize('store current', "{0}: Store Current Edit Session", EDIT_SESSION_SYNC_TITLE), original: 'Edit Sessions' }, +}; +const continueEditSessionCommand = { + id: '_workbench.experimental.editSessions.actions.continueEditSession', + title: { value: localize('continue edit session', "Continue Edit Session..."), original: 'Continue Edit Session...' }, +}; +const openLocalFolderCommand = { + id: '_workbench.experimental.editSessions.actions.continueEditSession.openLocalFolder', + title: localize('continue edit session in local folder', "Open In Local Folder"), +}; +const queryParamName = 'editSessionId'; + +export class SessionSyncContribution extends Disposable implements IWorkbenchContribution { + + private registered = false; + private continueEditSessionOptions: ContinueEditSessionItem[] = []; + + constructor( + @ISessionSyncWorkbenchService private readonly sessionSyncWorkbenchService: ISessionSyncWorkbenchService, + @IFileService private readonly fileService: IFileService, + @IProgressService private readonly progressService: IProgressService, + @IOpenerService private readonly openerService: IOpenerService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ISCMService private readonly scmService: ISCMService, + @INotificationService private readonly notificationService: INotificationService, + @IDialogService private readonly dialogService: IDialogService, + @ILogService private readonly logService: ILogService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IProductService private readonly productService: IProductService, + @IConfigurationService private configurationService: IConfigurationService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @ICommandService private commandService: ICommandService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IFileDialogService private readonly fileDialogService: IFileDialogService + ) { + super(); + + if (this.environmentService.editSessionId !== undefined) { + void this.applyEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined); + } + + this.configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('workbench.experimental.editSessions.enabled')) { + this.registerActions(); + } + }); + + this.registerActions(); + + continueEditSessionExtPoint.setHandler(extensions => { + const continueEditSessionOptions: ContinueEditSessionItem[] = []; + for (const extension of extensions) { + if (!isProposedApiEnabled(extension.description, 'contribEditSessions')) { + continue; + } + if (!Array.isArray(extension.value)) { + continue; + } + const commands = new Map((extension.description.contributes?.commands ?? []).map(c => [c.command, c])); + for (const contribution of extension.value) { + if (!contribution.command || !contribution.group || !contribution.when) { + continue; + } + const fullCommand = commands.get(contribution.command); + if (!fullCommand) { return; } + + continueEditSessionOptions.push(new ContinueEditSessionItem( + fullCommand.title, + fullCommand.command, + ContextKeyExpr.deserialize(contribution.when) + )); + } + } + this.continueEditSessionOptions = continueEditSessionOptions; + }); + } + + private registerActions() { + if (this.registered || this.configurationService.getValue('workbench.experimental.editSessions.enabled') !== true) { + return; + } + + this.registerContinueEditSessionAction(); + + this.registerApplyLatestEditSessionAction(); + this.registerStoreLatestEditSessionAction(); + + this.registerContinueInLocalFolderAction(); + + this.registered = true; + } + + private registerContinueEditSessionAction() { + const that = this; + this._register(registerAction2(class ContinueEditSessionAction extends Action2 { + constructor() { + super({ + id: continueEditSessionCommand.id, + title: continueEditSessionCommand.title, + f1: true + }); + } + + async run(accessor: ServicesAccessor, workspaceUri: URI | undefined): Promise<void> { + let uri = workspaceUri ?? await that.pickContinueEditSessionDestination(); + if (uri === undefined) { return; } + + // Run the store action to get back a ref + const ref = await that.storeEditSession(false); + + // Append the ref to the URI + if (ref !== undefined) { + const encodedRef = encodeURIComponent(ref); + uri = uri.with({ + query: uri.query.length > 0 ? (uri + `&${queryParamName}=${encodedRef}`) : `${queryParamName}=${encodedRef}` + }); + } else { + that.logService.warn(`Edit Sessions: Failed to store edit session when invoking ${continueEditSessionCommand.id}.`); + } + + // Open the URI + that.logService.info(`Edit Sessions: opening ${uri.toString()}`); + await that.openerService.open(uri, { openExternal: true }); + } + })); + } + + private registerApplyLatestEditSessionAction(): void { + const that = this; + this._register(registerAction2(class ApplyLatestEditSessionAction extends Action2 { + constructor() { + super({ + id: resumeLatestCommand.id, + title: resumeLatestCommand.title, + menu: { + id: MenuId.CommandPalette, + } + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + await that.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('applying edit session', 'Applying edit session...') + }, async () => await that.applyEditSession()); + } + })); + } + + private registerStoreLatestEditSessionAction(): void { + const that = this; + this._register(registerAction2(class StoreLatestEditSessionAction extends Action2 { + constructor() { + super({ + id: storeCurrentCommand.id, + title: storeCurrentCommand.title, + menu: { + id: MenuId.CommandPalette, + } + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + await that.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('storing edit session', 'Storing edit session...') + }, async () => await that.storeEditSession(true)); + } + })); + } + + async applyEditSession(ref?: string): Promise<void> { + if (ref !== undefined) { + this.logService.info(`Edit Sessions: Applying edit session with ref ${ref}.`); + } + + const data = await this.sessionSyncWorkbenchService.read(ref); + if (!data) { + if (ref === undefined) { + this.notificationService.info(localize('no edit session', 'There are no edit sessions to apply.')); + } else { + this.notificationService.warn(localize('no edit session content for ref', 'Could not apply edit session contents for ID {0}.', ref)); + } + this.logService.info(`Edit Sessions: Aborting applying edit session as no edit session content is available to be applied from ref ${ref}.`); + return; + } + const editSession = data.editSession; + ref = data.ref; + + if (editSession.version > EditSessionSchemaVersion) { + this.notificationService.error(localize('client too old', "Please upgrade to a newer version of {0} to apply this edit session.", this.productService.nameLong)); + return; + } + + try { + const changes: ({ uri: URI; type: ChangeType; contents: string | undefined })[] = []; + let hasLocalUncommittedChanges = false; + + for (const folder of editSession.folders) { + const folderRoot = this.contextService.getWorkspace().folders.find((f) => f.name === folder.name); + if (!folderRoot) { + this.logService.info(`Edit Sessions: Skipping applying ${folder.workingChanges.length} changes from edit session with ref ${ref} as no corresponding workspace folder named ${folder.name} is currently open.`); + continue; + } + + for (const repository of this.scmService.repositories) { + if (repository.provider.rootUri !== undefined && + this.contextService.getWorkspaceFolder(repository.provider.rootUri)?.name === folder.name && + this.getChangedResources(repository).length > 0 + ) { + hasLocalUncommittedChanges = true; + break; + } + } + + for (const { relativeFilePath, contents, type } of folder.workingChanges) { + const uri = joinPath(folderRoot.uri, relativeFilePath); + changes.push({ uri: uri, type: type, contents: contents }); + } + } + + if (hasLocalUncommittedChanges) { + // TODO@joyceerhl Provide the option to diff files which would be overwritten by edit session contents + const result = await this.dialogService.confirm({ + message: localize('apply edit session warning', 'Applying your edit session may overwrite your existing uncommitted changes. Do you want to proceed?'), + type: 'warning', + title: EDIT_SESSION_SYNC_TITLE + }); + if (!result.confirmed) { + return; + } + } + + for (const { uri, type, contents } of changes) { + if (type === ChangeType.Addition) { + await this.fileService.writeFile(uri, VSBuffer.fromString(contents!)); + } else if (type === ChangeType.Deletion && await this.fileService.exists(uri)) { + await this.fileService.del(uri); + } + } + + this.logService.info(`Edit Sessions: Deleting edit session with ref ${ref} after successfully applying it to current workspace...`); + await this.sessionSyncWorkbenchService.delete(ref); + this.logService.info(`Edit Sessions: Deleted edit session with ref ${ref}.`); + } catch (ex) { + this.logService.error('Edit Sessions: Failed to apply edit session, reason: ', (ex as Error).toString()); + this.notificationService.error(localize('apply failed', "Failed to apply your edit session.")); + } + } + + async storeEditSession(fromStoreCommand: boolean): Promise<string | undefined> { + const folders: Folder[] = []; + let hasEdits = false; + + for (const repository of this.scmService.repositories) { + // Look through all resource groups and compute which files were added/modified/deleted + const trackedUris = this.getChangedResources(repository); // A URI might appear in more than one resource group + + const workingChanges: Change[] = []; + let name = repository.provider.rootUri ? this.contextService.getWorkspaceFolder(repository.provider.rootUri)?.name : undefined; + + for (const uri of trackedUris) { + const workspaceFolder = this.contextService.getWorkspaceFolder(uri); + if (!workspaceFolder) { + this.logService.info(`Edit Sessions: Skipping working change ${uri.toString()} as no associated workspace folder was found.`); + + continue; + } + + name = name ?? workspaceFolder.name; + const relativeFilePath = relativePath(workspaceFolder.uri, uri) ?? uri.path; + + // Only deal with file contents for now + try { + if (!(await this.fileService.stat(uri)).isFile) { + continue; + } + } catch { } + + hasEdits = true; + + if (await this.fileService.exists(uri)) { + workingChanges.push({ type: ChangeType.Addition, fileType: FileType.File, contents: (await this.fileService.readFile(uri)).value.toString(), relativeFilePath: relativeFilePath }); + } else { + // Assume it's a deletion + workingChanges.push({ type: ChangeType.Deletion, fileType: FileType.File, contents: undefined, relativeFilePath: relativeFilePath }); + } + } + + folders.push({ workingChanges, name: name ?? '' }); + } + + if (!hasEdits) { + this.logService.info('Edit Sessions: Skipping storing edit session as there are no edits to store.'); + if (fromStoreCommand) { + this.notificationService.info(localize('no edits to store', 'Skipped storing edit session as there are no edits to store.')); + } + return undefined; + } + + const data: EditSession = { folders, version: 1 }; + + try { + this.logService.info(`Edit Sessions: Storing edit session...`); + const ref = await this.sessionSyncWorkbenchService.write(data); + this.logService.info(`Edit Sessions: Stored edit session with ref ${ref}.`); + return ref; + } catch (ex) { + this.logService.error(`Edit Sessions: Failed to store edit session, reason: `, (ex as Error).toString()); + + type UploadFailedEvent = { reason: string }; + type UploadFailedClassification = { + owner: 'joyceerhl'; comment: 'Reporting when Continue On server request fails.'; + reason?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The reason that the server request failed.' }; + }; + + if (ex instanceof UserDataSyncStoreError) { + switch (ex.code) { + case UserDataSyncErrorCode.TooLarge: + // Uploading a payload can fail due to server size limits + this.telemetryService.publicLog2<UploadFailedEvent, UploadFailedClassification>('sessionSync.upload.failed', { reason: 'TooLarge' }); + this.notificationService.error(localize('payload too large', 'Your edit session exceeds the size limit and cannot be stored.')); + break; + default: + this.telemetryService.publicLog2<UploadFailedEvent, UploadFailedClassification>('sessionSync.upload.failed', { reason: 'unknown' }); + this.notificationService.error(localize('payload failed', 'Your edit session cannot be stored.')); + break; + } + } + } + + return undefined; + } + + private getChangedResources(repository: ISCMRepository) { + const trackedUris = repository.provider.groups.elements.reduce((resources, resourceGroups) => { + resourceGroups.elements.forEach((resource) => resources.add(resource.sourceUri)); + return resources; + }, new Set<URI>()); // A URI might appear in more than one resource group + + return [...trackedUris]; + } + + //#region Continue Edit Session extension contribution point + + private registerContinueInLocalFolderAction(): void { + const that = this; + this._register(registerAction2(class ContinueInLocalFolderAction extends Action2 { + constructor() { + super({ + id: openLocalFolderCommand.id, + title: openLocalFolderCommand.title, + precondition: IsWebContext + }); + } + + async run(accessor: ServicesAccessor): Promise<URI | undefined> { + const selection = await that.fileDialogService.showOpenDialog({ + title: localize('continueEditSession.openLocalFolder.title', 'Select a local folder to continue your edit session in'), + canSelectFolders: true, + canSelectMany: false, + canSelectFiles: false, + availableFileSystems: [Schemas.file] + }); + + return selection?.length !== 1 ? undefined : URI.from({ + scheme: that.productService.urlProtocol, + authority: Schemas.file, + path: selection[0].path + }); + } + })); + } + + private async pickContinueEditSessionDestination(): Promise<URI | undefined> { + const quickPick = this.quickInputService.createQuickPick<ContinueEditSessionItem>(); + + quickPick.title = localize('continueEditSessionPick.title', 'Continue Edit Session...'); + quickPick.placeholder = localize('continueEditSessionPick.placeholder', 'Choose how you would like to continue working'); + quickPick.items = this.createPickItems(); + + const command = await new Promise<string | undefined>((resolve, reject) => { + quickPick.onDidHide(() => resolve(undefined)); + + quickPick.onDidAccept((e) => { + const selection = quickPick.activeItems[0].command; + resolve(selection); + quickPick.hide(); + }); + + quickPick.show(); + }); + + quickPick.dispose(); + + if (command === undefined) { + return undefined; + } + + try { + const uri = await this.commandService.executeCommand(command); + return URI.isUri(uri) ? uri : undefined; + } catch (ex) { + return undefined; + } + } + + private createPickItems(): ContinueEditSessionItem[] { + const items = [...this.continueEditSessionOptions].filter((option) => option.when === undefined || this.contextKeyService.contextMatchesRules(option.when)); + + if (getVirtualWorkspaceLocation(this.contextService.getWorkspace()) !== undefined) { + items.push(new ContinueEditSessionItem( + localize('continueEditSessionItem.openInLocalFolder', 'Open In Local Folder'), + openLocalFolderCommand.id, + )); + } + + return items; + } +} + +class ContinueEditSessionItem implements IQuickPickItem { + constructor( + public readonly label: string, + public readonly command: string, + public readonly when?: ContextKeyExpression, + ) { } +} + +interface ICommand { + command: string; + group: string; + when: string; +} + +const continueEditSessionExtPoint = ExtensionsRegistry.registerExtensionPoint<ICommand[]>({ + extensionPoint: 'continueEditSession', + jsonSchema: { + description: localize('continueEditSessionExtPoint', 'Contributes options for continuing the current edit session in a different environment'), + type: 'array', + items: { + type: 'object', + properties: { + command: { + description: localize('continueEditSessionExtPoint.command', 'Identifier of the command to execute. The command must be declared in the \'commands\'-section and return a URI representing a different environment where the current edit session can be continued.'), + type: 'string' + }, + group: { + description: localize('continueEditSessionExtPoint.group', 'Group into which this item belongs.'), + type: 'string' + }, + when: { + description: localize('continueEditSessionExtPoint.when', 'Condition which must be true to show this item.'), + type: 'string' + } + }, + required: ['command'] + } + } +}); + +//#endregion + +const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(SessionSyncContribution, LifecyclePhase.Restored); + +Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({ + ...workbenchConfigurationNodeBase, + 'properties': { + 'workbench.experimental.editSessions.enabled': { + 'type': 'boolean', + 'tags': ['experimental', 'usesOnlineServices'], + 'default': false, + 'markdownDescription': localize('editSessionsEnabled', "Controls whether to display cloud-enabled actions to store and resume uncommitted changes when switching between web, desktop, or devices."), + }, + } +}); diff --git a/src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts b/src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts new file mode 100644 index 00000000000..5c29394116b --- /dev/null +++ b/src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { Schemas } from 'vs/base/common/network'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { NullLogService, ILogService } from 'vs/platform/log/common/log'; +import { SessionSyncContribution } from 'vs/workbench/contrib/sessionSync/browser/sessionSync.contribution'; +import { ProgressService } from 'vs/workbench/services/progress/browser/progressService'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; +import { SCMService } from 'vs/workbench/contrib/scm/common/scmService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { mock } from 'vs/base/test/common/mock'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { ChangeType, FileType, ISessionSyncWorkbenchService } from 'vs/workbench/services/sessionSync/common/sessionSync'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; + +const folderName = 'test-folder'; +const folderUri = URI.file(`/${folderName}`); + +suite('Edit session sync', () => { + let instantiationService: TestInstantiationService; + let sessionSyncContribution: SessionSyncContribution; + let fileService: FileService; + let sandbox: sinon.SinonSandbox; + + const disposables = new DisposableStore(); + + suiteSetup(() => { + sandbox = sinon.createSandbox(); + + instantiationService = new TestInstantiationService(); + + // Set up filesystem + const logService = new NullLogService(); + fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(Schemas.file, fileSystemProvider); + + // Stub out all services + instantiationService.stub(ILogService, logService); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(INotificationService, new TestNotificationService()); + instantiationService.stub(ISessionSyncWorkbenchService, new class extends mock<ISessionSyncWorkbenchService>() { }); + instantiationService.stub(IProgressService, ProgressService); + instantiationService.stub(ISCMService, SCMService); + instantiationService.stub(IEnvironmentService, TestEnvironmentService); + instantiationService.stub(IConfigurationService, new TestConfigurationService({ workbench: { experimental: { sessionSync: { enabled: true } } } })); + instantiationService.stub(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { + override getWorkspace() { + return { + id: 'workspace-id', + folders: [{ + uri: folderUri, + name: folderName, + index: 0, + toResource: (relativePath: string) => joinPath(folderUri, relativePath) + }] + }; + } + }); + + // Stub repositories + instantiationService.stub(ISCMService, '_repositories', new Map()); + + sessionSyncContribution = instantiationService.createInstance(SessionSyncContribution); + }); + + teardown(() => { + sinon.restore(); + disposables.clear(); + }); + + test('Can apply edit session', async function () { + const fileUri = joinPath(folderUri, 'dir1', 'README.md'); + const fileContents = '# readme'; + const editSession = { + version: 1, + folders: [ + { + name: folderName, + workingChanges: [ + { + relativeFilePath: 'dir1/README.md', + fileType: FileType.File, + contents: fileContents, + type: ChangeType.Addition + } + ] + } + ] + }; + + // Stub sync service to return edit session data + const readStub = sandbox.stub().returns({ editSession, ref: '0' }); + instantiationService.stub(ISessionSyncWorkbenchService, 'read', readStub); + + // Create root folder + await fileService.createFolder(folderUri); + + // Apply edit session + await sessionSyncContribution.applyEditSession(); + + // Verify edit session was correctly applied + assert.equal((await fileService.readFile(fileUri)).value.toString(), fileContents); + }); + + test('Edit session not stored if there are no edits', async function () { + const writeStub = sandbox.stub(); + instantiationService.stub(ISessionSyncWorkbenchService, 'write', writeStub); + + // Create root folder + await fileService.createFolder(folderUri); + + await sessionSyncContribution.storeEditSession(true); + + // Verify that we did not attempt to write the edit session + assert.equal(writeStub.called, false); + }); +}); diff --git a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts index 9b5871c2e89..917e0da44e4 100644 --- a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { extname } from 'vs/base/common/path'; import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; @@ -19,6 +18,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { isValidBasename } from 'vs/base/common/extpath'; import { joinPath, basename } from 'vs/base/common/resources'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; namespace ISnippetPick { export function is(thing: object | undefined): thing is ISnippetPick { @@ -31,7 +31,7 @@ interface ISnippetPick extends IQuickPickItem { hint?: true; } -async function computePicks(snippetService: ISnippetsService, envService: IEnvironmentService, languageService: ILanguageService) { +async function computePicks(snippetService: ISnippetsService, userDataProfileService: IUserDataProfileService, languageService: ILanguageService) { const existing: ISnippetPick[] = []; const future: ISnippetPick[] = []; @@ -85,7 +85,7 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir } } - const dir = envService.snippetsHome; + const dir = userDataProfileService.currentProfile.snippetsHome; for (const languageId of languageService.getRegisteredLanguageIds()) { const label = languageService.getLanguageName(languageId); if (label && !seen.has(languageId)) { @@ -99,8 +99,8 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir } existing.sort((a, b) => { - let a_ext = extname(a.filepath.path); - let b_ext = extname(b.filepath.path); + const a_ext = extname(a.filepath.path); + const b_ext = extname(b.filepath.path); if (a_ext === b_ext) { return a.label.localeCompare(b.label); } else if (a_ext === '.code-snippets') { @@ -227,19 +227,19 @@ registerAction2(class ConfigureSnippets extends Action2 { const quickInputService = accessor.get(IQuickInputService); const opener = accessor.get(IOpenerService); const languageService = accessor.get(ILanguageService); - const envService = accessor.get(IEnvironmentService); + const userDataProfileService = accessor.get(IUserDataProfileService); const workspaceService = accessor.get(IWorkspaceContextService); const fileService = accessor.get(IFileService); const textFileService = accessor.get(ITextFileService); - const picks = await computePicks(snippetService, envService, languageService); + const picks = await computePicks(snippetService, userDataProfileService, languageService); const existing: QuickPickInput[] = picks.existing; type SnippetPick = IQuickPickItem & { uri: URI } & { scope: string }; const globalSnippetPicks: SnippetPick[] = [{ scope: nls.localize('new.global_scope', 'global'), label: nls.localize('new.global', "New Global Snippets file..."), - uri: envService.snippetsHome + uri: userDataProfileService.currentProfile.snippetsHome }]; const workspaceSnippetPicks: SnippetPick[] = []; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 2b7c3c8ed98..3e8ea30e666 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -148,7 +148,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { if (!triggerCharacterLow) { const endsInWhitespace = /\s/.test(lineContentLow[position.column - 2]); if (endsInWhitespace || !lineContentLow /*empty line*/) { - for (let snippet of snippets) { + for (const snippet of snippets) { const insert = Range.fromPositions(position); const replace = lineContentLow.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert; suggestions.push(new SnippetCompletion(snippet, { replace, insert })); @@ -160,7 +160,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { // dismbiguate suggestions with same labels suggestions.sort(SnippetCompletion.compareByLabel); for (let i = 0; i < suggestions.length; i++) { - let item = suggestions[i]; + const item = suggestions[i]; let to = i + 1; for (; to < suggestions.length && item.label === suggestions[to].label; to++) { suggestions[to].label.label = localize('snippetSuggest.longLabel', "{0}, {1}", suggestions[to].label.label, suggestions[to].snippet.name); diff --git a/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts b/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts index 9c83465a206..9cb08b37047 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts @@ -87,6 +87,9 @@ export async function pickSnippet(accessor: ServicesAccessor, languageIdOrSnippe picker.items = makeSnippetPicks(); }); picker.items = makeSnippetPicks(); + if (!picker.items.length) { + picker.validationMessage = nls.localize('pick.noSnippetAvailable', "No snippet available"); + } picker.show(); // wait for an item to be picked or the picker to become hidden diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index 84f407b5f3e..eb1497201da 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -45,7 +45,7 @@ class SnippetBodyInsights { // check snippet... const textmateSnippet = new SnippetParser().parse(body, false); - let placeholders = new Map<string, number>(); + const placeholders = new Map<string, number>(); let placeholderMax = 0; for (const placeholder of textmateSnippet.placeholders) { placeholderMax = Math.max(placeholderMax, placeholder.index); @@ -60,7 +60,7 @@ class SnippetBodyInsights { this.isTrivial = last instanceof Placeholder && last.isFinalTabstop; } - let stack = [...textmateSnippet.children]; + const stack = [...textmateSnippet.children]; while (stack.length > 0) { const marker = stack.shift()!; if (marker instanceof Variable) { @@ -236,7 +236,7 @@ export class SnippetFile { } } - let idx = selector.lastIndexOf('.'); + const idx = selector.lastIndexOf('.'); if (idx >= 0) { this._scopeSelect(selector.substring(0, idx), bucket); } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 26e453f1b1e..366ed64536b 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -30,6 +30,7 @@ import { isStringArray } from 'vs/base/common/types'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; namespace snippetExt { @@ -138,7 +139,7 @@ class SnippetEnablement { @IStorageService private readonly _storageService: IStorageService, ) { - const raw = _storageService.get(SnippetEnablement._key, StorageScope.GLOBAL, ''); + const raw = _storageService.get(SnippetEnablement._key, StorageScope.PROFILE, ''); let data: string[] | undefined; try { data = JSON.parse(raw); @@ -161,7 +162,7 @@ class SnippetEnablement { changed = true; } if (changed) { - this._storageService.store(SnippetEnablement._key, JSON.stringify(Array.from(this._ignored)), StorageScope.GLOBAL, StorageTarget.USER); + this._storageService.store(SnippetEnablement._key, JSON.stringify(Array.from(this._ignored)), StorageScope.PROFILE, StorageTarget.USER); } } } @@ -177,6 +178,7 @@ class SnippetsService implements ISnippetsService { constructor( @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @ILanguageService private readonly _languageService: ILanguageService, @ILogService private readonly _logService: ILogService, @@ -318,8 +320,8 @@ class SnippetsService implements ISnippetsService { private _initWorkspaceSnippets(): void { // workspace stuff - let disposables = new DisposableStore(); - let updateWorkspaceSnippets = () => { + const disposables = new DisposableStore(); + const updateWorkspaceSnippets = () => { disposables.clear(); this._pendingWork.push(this._initWorkspaceFolderSnippets(this._contextService.getWorkspace(), disposables)); }; @@ -348,9 +350,16 @@ class SnippetsService implements ISnippetsService { } private async _initUserSnippets(): Promise<any> { - const userSnippetsFolder = this._environmentService.snippetsHome; - await this._fileService.createFolder(userSnippetsFolder); - return await this._initFolderSnippets(SnippetSource.User, userSnippetsFolder, this._disposables); + const disposables = new DisposableStore(); + const updateUserSnippets = async () => { + disposables.clear(); + const userSnippetsFolder = this._userDataProfileService.currentProfile.snippetsHome; + await this._fileService.createFolder(userSnippetsFolder); + await this._initFolderSnippets(SnippetSource.User, userSnippetsFolder, disposables); + }; + this._disposables.add(disposables); + this._disposables.add(this._userDataProfileService.onDidChangeCurrentProfile(() => this._pendingWork.push(updateUserSnippets()))); + await updateUserSnippets(); } private _initFolderSnippets(source: SnippetSource, folder: URI, bucket: DisposableStore): Promise<any> { @@ -406,11 +415,11 @@ export function getNonWhitespacePrefix(model: ISimpleModel, position: Position): */ const MAX_PREFIX_LENGTH = 100; - let line = model.getLineContent(position.lineNumber).substr(0, position.column - 1); + const line = model.getLineContent(position.lineNumber).substr(0, position.column - 1); - let minChIndex = Math.max(0, line.length - MAX_PREFIX_LENGTH); + const minChIndex = Math.max(0, line.length - MAX_PREFIX_LENGTH); for (let chIndex = line.length - 1; chIndex >= minChIndex; chIndex--) { - let ch = line.charAt(chIndex); + const ch = line.charAt(chIndex); if (/\s/.test(ch)) { return line.substr(chIndex + 1); diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts index eac4af56816..dffa5ea30ef 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts @@ -55,12 +55,12 @@ suite('Snippets', function () { test('SnippetFile#select - any scope', function () { - let file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ + const file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ new Snippet([], 'AnySnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User), new Snippet(['foo'], 'FooSnippet1', 'foo', '', 'snippet', 'test', SnippetSource.User), ]); - let bucket: Snippet[] = []; + const bucket: Snippet[] = []; file.select('foo', bucket); assert.strictEqual(bucket.length, 2); @@ -69,7 +69,7 @@ suite('Snippets', function () { test('Snippet#needsClipboard', function () { function assertNeedsClipboard(body: string, expected: boolean): void { - let snippet = new Snippet(['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User); + const snippet = new Snippet(['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User); assert.strictEqual(snippet.needsClipboard, expected); assert.strictEqual(SnippetParser.guessNeedsClipboard(body), expected); @@ -86,7 +86,7 @@ suite('Snippets', function () { test('Snippet#isTrivial', function () { function assertIsTrivial(body: string, expected: boolean): void { - let snippet = new Snippet(['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User); + const snippet = new Snippet(['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User); assert.strictEqual(snippet.isTrivial, expected); } diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts index b8b94f7b3be..8a9d39422e4 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsRegistry.test.ts @@ -10,10 +10,10 @@ import { Position } from 'vs/editor/common/core/position'; suite('getNonWhitespacePrefix', () => { function assertGetNonWhitespacePrefix(line: string, column: number, expected: string): void { - let model = { + const model = { getLineContent: (lineNumber: number) => line }; - let actual = getNonWhitespacePrefix(model, new Position(1, column)); + const actual = getNonWhitespacePrefix(model, new Position(1, column)); assert.strictEqual(actual, expected); } 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 9a602558814..6c442beb9f2 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -246,7 +246,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(instantiateTextModel(instantiationService, '<head>\n\t\n>/head>', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, '<head>\n\t\n>/head>', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.suggestions.length, 1); return provider.provideCompletionItems(model, new Position(2, 2), context)!; @@ -276,10 +276,10 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.suggestions.length, 2); - let [first, second] = result.suggestions; + const [first, second] = result.suggestions; assert.deepStrictEqual(first.label, { label: 'first', description: 'first' @@ -303,7 +303,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(instantiateTextModel(instantiationService, 'p-', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, 'p-', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -328,8 +328,8 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', 'fooLang')); - let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; + const model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', 'fooLang')); + const result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.strictEqual(result.suggestions.length, 1); }); @@ -347,8 +347,8 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(instantiateTextModel(instantiationService, ':', 'fooLang')); - let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; + const model = disposables.add(instantiateTextModel(instantiationService, ':', 'fooLang')); + const result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 0); }); @@ -366,8 +366,8 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(instantiateTextModel(instantiationService, 'template', 'fooLang')); - let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; + const model = disposables.add(instantiateTextModel(instantiationService, 'template', 'fooLang')); + const result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.strictEqual(result.suggestions.length, 1); assert.deepStrictEqual(result.suggestions[0].label, { @@ -389,8 +389,8 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', 'fooLang')); - let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; + const model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', 'fooLang')); + const result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.strictEqual(result.suggestions.length, 1); }); @@ -413,8 +413,8 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); - let model = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', 'fooLang')); - let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!; + const model = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', 'fooLang')); + const result = await provider.provideCompletionItems(model, new Position(1, 8), context)!; assert.strictEqual(result.suggestions.length, 1); }); @@ -432,8 +432,8 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(instantiateTextModel(instantiationService, 'a ', 'fooLang')); - let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; + const model = disposables.add(instantiateTextModel(instantiationService, 'a ', 'fooLang')); + const result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); }); @@ -531,11 +531,11 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, 'filler e KEEP ng filler', 'fooLang'); - let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; + const model = instantiateTextModel(instantiationService, 'filler e KEEP ng filler', 'fooLang'); + const result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.strictEqual(result.suggestions.length, 1); - let [first] = result.suggestions; + const [first] = result.suggestions; assert.strictEqual((first.range as any).insert.endColumn, 9); assert.strictEqual((first.range as any).replace.endColumn, 9); model.dispose(); @@ -563,11 +563,11 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); - let model = instantiateTextModel(instantiationService, '[psc]', 'fooLang'); - let result = await provider.provideCompletionItems(model, new Position(1, 5), context)!; + const model = instantiateTextModel(instantiationService, '[psc]', 'fooLang'); + const result = await provider.provideCompletionItems(model, new Position(1, 5), context)!; assert.strictEqual(result.suggestions.length, 1); - let [first] = result.suggestions; + const [first] = result.suggestions; assert.strictEqual((first.range as any).insert.endColumn, 5); // This is 6 because it should eat the `]` at the end of the text even if cursor is before it assert.strictEqual((first.range as any).replace.endColumn, 6); @@ -588,11 +588,11 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, ' ci', 'fooLang'); - let result = await provider.provideCompletionItems(model, new Position(1, 4), context)!; + const model = instantiateTextModel(instantiationService, ' ci', 'fooLang'); + const result = await provider.provideCompletionItems(model, new Position(1, 4), context)!; assert.strictEqual(result.suggestions.length, 1); - let [first] = result.suggestions; + const [first] = result.suggestions; assert.strictEqual((<CompletionItemLabel>first.label).label, ' cite'); assert.strictEqual((<CompletionItemRanges>first.range).insert.startColumn, 1); @@ -609,8 +609,8 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); - let result = await provider.provideCompletionItems( + const model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); + const result = await provider.provideCompletionItems( model, new Position(1, 2), { triggerKind: CompletionTriggerKind.TriggerCharacter, triggerCharacter: '\'' } @@ -631,9 +631,9 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); + const model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); - let result = await provider.provideCompletionItems( + const result = await provider.provideCompletionItems( model, new Position(1, 2), { triggerKind: CompletionTriggerKind.TriggerCharacter, triggerCharacter: '\'' } @@ -651,9 +651,9 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, '\'hellot\'', 'fooLang'); + const model = instantiateTextModel(instantiationService, '\'hellot\'', 'fooLang'); - let result = await provider.provideCompletionItems( + const result = await provider.provideCompletionItems( model, new Position(1, 8), { triggerKind: CompletionTriggerKind.Invoke } @@ -672,9 +672,9 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, ')*&^', 'fooLang'); + const model = instantiateTextModel(instantiationService, ')*&^', 'fooLang'); - let result = await provider.provideCompletionItems( + const result = await provider.provideCompletionItems( model, new Position(1, 5), { triggerKind: CompletionTriggerKind.Invoke } @@ -692,9 +692,9 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, 'foobar', 'fooLang'); + const model = instantiateTextModel(instantiationService, 'foobar', 'fooLang'); - let result = await provider.provideCompletionItems( + const result = await provider.provideCompletionItems( model, new Position(1, 7), { triggerKind: CompletionTriggerKind.Invoke } @@ -712,8 +712,8 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, 'function abc(w)', 'fooLang'); - let result = await provider.provideCompletionItems( + const model = instantiateTextModel(instantiationService, 'function abc(w)', 'fooLang'); + const result = await provider.provideCompletionItems( model, new Position(1, 15), { triggerKind: CompletionTriggerKind.Invoke } @@ -731,8 +731,8 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = instantiateTextModel(instantiationService, 'di', 'fooLang'); - let result = await provider.provideCompletionItems( + const model = instantiateTextModel(instantiationService, 'di', 'fooLang'); + const result = await provider.provideCompletionItems( model, new Position(1, 3), { triggerKind: CompletionTriggerKind.Invoke } @@ -743,7 +743,7 @@ suite('SnippetsService', function () { model.applyEdits([EditOperation.insert(new Position(1, 3), '.')]); assert.strictEqual(model.getValue(), 'di.'); - let result2 = await provider.provideCompletionItems( + const result2 = await provider.provideCompletionItems( model, new Position(1, 4), { triggerKind: CompletionTriggerKind.TriggerCharacter, triggerCharacter: '.' } diff --git a/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts b/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts index 6c2dab81268..3320a020843 100644 --- a/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/ces.contribution.ts @@ -48,7 +48,7 @@ class CESContribution extends Disposable implements IWorkbenchContribution { return; } - const skipSurvey = storageService.get(SKIP_SURVEY_KEY, StorageScope.GLOBAL, ''); + const skipSurvey = storageService.get(SKIP_SURVEY_KEY, StorageScope.APPLICATION, ''); if (skipSurvey) { return; } @@ -66,6 +66,7 @@ class CESContribution extends Disposable implements IWorkbenchContribution { const sendTelemetry = (userReaction: 'accept' | 'remindLater' | 'neverShowAgain' | 'cancelled') => { /* __GDPR__ "cesSurvey:popup" : { + "owner": "digitarald", "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -88,7 +89,7 @@ class CESContribution extends Disposable implements IWorkbenchContribution { const usedParams = this.productService.surveys ?.filter(surveyData => surveyData.surveyId && surveyData.languageId) // Counts provided by contrib/surveys/browser/languageSurveys - .filter(surveyData => this.storageService.getNumber(`${surveyData.surveyId}.editedCount`, StorageScope.GLOBAL, 0) > 0) + .filter(surveyData => this.storageService.getNumber(`${surveyData.surveyId}.editedCount`, StorageScope.APPLICATION, 0) > 0) .map(surveyData => `${encodeURIComponent(surveyData.languageId)}Lang=1`) .join('&'); if (usedParams) { @@ -102,7 +103,7 @@ class CESContribution extends Disposable implements IWorkbenchContribution { label: nls.localize('remindLater', "Remind Me later"), run: () => { sendTelemetry('remindLater'); - this.storageService.store(REMIND_LATER_DATE_KEY, new Date().toUTCString(), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(REMIND_LATER_DATE_KEY, new Date().toUTCString(), StorageScope.APPLICATION, StorageTarget.USER); this.schedulePrompt(); } }], @@ -120,7 +121,7 @@ class CESContribution extends Disposable implements IWorkbenchContribution { private async schedulePrompt(): Promise<void> { let waitTimeToShowSurvey = 0; - const remindLaterDate = this.storageService.get(REMIND_LATER_DATE_KEY, StorageScope.GLOBAL, ''); + const remindLaterDate = this.storageService.get(REMIND_LATER_DATE_KEY, StorageScope.APPLICATION, ''); if (remindLaterDate) { const timeToRemind = new Date(remindLaterDate).getTime() + REMIND_LATER_DELAY - Date.now(); if (timeToRemind > 0) { @@ -141,7 +142,9 @@ class CESContribution extends Disposable implements IWorkbenchContribution { } } /* __GDPR__ - "cesSurvey:schedule" : { } + "cesSurvey:schedule" : { + "owner": "digitarald" + } */ this.telemetryService.publicLog('cesSurvey:schedule'); @@ -151,7 +154,7 @@ class CESContribution extends Disposable implements IWorkbenchContribution { } private skipSurvey(): void { - this.storageService.store(SKIP_SURVEY_KEY, this.productService.version, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(SKIP_SURVEY_KEY, this.productService.version, StorageScope.APPLICATION, StorageTarget.USER); } } diff --git a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts index 75d75f97b34..ae6e3b498bc 100644 --- a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts @@ -43,22 +43,22 @@ class LanguageSurvey extends Disposable { const EDITED_LANGUAGE_COUNT_KEY = `${data.surveyId}.editedCount`; const EDITED_LANGUAGE_DATE_KEY = `${data.surveyId}.editedDate`; - const skipVersion = storageService.get(SKIP_VERSION_KEY, StorageScope.GLOBAL, ''); + const skipVersion = storageService.get(SKIP_VERSION_KEY, StorageScope.APPLICATION, ''); if (skipVersion) { return; } const date = new Date().toDateString(); - if (storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) < data.editCount) { + if (storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.APPLICATION, 0) < data.editCount) { // Process model-save event every 250ms to reduce load const onModelsSavedWorker = this._register(new RunOnceWorker<ITextFileEditorModel>(models => { models.forEach(m => { - if (m.getLanguageId() === data.languageId && date !== storageService.get(EDITED_LANGUAGE_DATE_KEY, StorageScope.GLOBAL)) { - const editedCount = storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) + 1; - storageService.store(EDITED_LANGUAGE_COUNT_KEY, editedCount, StorageScope.GLOBAL, StorageTarget.USER); - storageService.store(EDITED_LANGUAGE_DATE_KEY, date, StorageScope.GLOBAL, StorageTarget.USER); + if (m.getLanguageId() === data.languageId && date !== storageService.get(EDITED_LANGUAGE_DATE_KEY, StorageScope.APPLICATION)) { + const editedCount = storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.APPLICATION, 0) + 1; + storageService.store(EDITED_LANGUAGE_COUNT_KEY, editedCount, StorageScope.APPLICATION, StorageTarget.USER); + storageService.store(EDITED_LANGUAGE_DATE_KEY, date, StorageScope.APPLICATION, StorageTarget.USER); } }); }, 250)); @@ -66,30 +66,30 @@ class LanguageSurvey extends Disposable { this._register(textFileService.files.onDidSave(e => onModelsSavedWorker.work(e.model))); } - const lastSessionDate = storageService.get(LAST_SESSION_DATE_KEY, StorageScope.GLOBAL, new Date(0).toDateString()); + const lastSessionDate = storageService.get(LAST_SESSION_DATE_KEY, StorageScope.APPLICATION, new Date(0).toDateString()); if (date === lastSessionDate) { return; } - const sessionCount = storageService.getNumber(SESSION_COUNT_KEY, StorageScope.GLOBAL, 0) + 1; - storageService.store(LAST_SESSION_DATE_KEY, date, StorageScope.GLOBAL, StorageTarget.USER); - storageService.store(SESSION_COUNT_KEY, sessionCount, StorageScope.GLOBAL, StorageTarget.USER); + const sessionCount = storageService.getNumber(SESSION_COUNT_KEY, StorageScope.APPLICATION, 0) + 1; + storageService.store(LAST_SESSION_DATE_KEY, date, StorageScope.APPLICATION, StorageTarget.USER); + storageService.store(SESSION_COUNT_KEY, sessionCount, StorageScope.APPLICATION, StorageTarget.USER); if (sessionCount < 9) { return; } - if (storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) < data.editCount) { + if (storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.APPLICATION, 0) < data.editCount) { return; } - const isCandidate = storageService.getBoolean(IS_CANDIDATE_KEY, StorageScope.GLOBAL, false) + const isCandidate = storageService.getBoolean(IS_CANDIDATE_KEY, StorageScope.APPLICATION, false) || Math.random() < data.userProbability; - storageService.store(IS_CANDIDATE_KEY, isCandidate, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(IS_CANDIDATE_KEY, isCandidate, StorageScope.APPLICATION, StorageTarget.USER); if (!isCandidate) { - storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.APPLICATION, StorageTarget.USER); return; } @@ -102,23 +102,23 @@ class LanguageSurvey extends Disposable { telemetryService.publicLog(`${data.surveyId}.survey/takeShortSurvey`); telemetryService.getTelemetryInfo().then(info => { openerService.open(URI.parse(`${data.surveyUrl}?o=${encodeURIComponent(platform)}&v=${encodeURIComponent(productService.version)}&m=${encodeURIComponent(info.machineId)}`)); - storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER); - storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(IS_CANDIDATE_KEY, false, StorageScope.APPLICATION, StorageTarget.USER); + storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.APPLICATION, StorageTarget.USER); }); } }, { label: localize('remindLater', "Remind Me later"), run: () => { telemetryService.publicLog(`${data.surveyId}.survey/remindMeLater`); - storageService.store(SESSION_COUNT_KEY, sessionCount - 3, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(SESSION_COUNT_KEY, sessionCount - 3, StorageScope.APPLICATION, StorageTarget.USER); } }, { label: localize('neverAgain', "Don't Show Again"), isSecondary: true, run: () => { telemetryService.publicLog(`${data.surveyId}.survey/dontShowAgain`); - storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER); - storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(IS_CANDIDATE_KEY, false, StorageScope.APPLICATION, StorageTarget.USER); + storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.APPLICATION, StorageTarget.USER); } }], { sticky: true } diff --git a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts index 243eb7500b6..bb1e79f25e4 100644 --- a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts @@ -35,33 +35,33 @@ class NPSContribution implements IWorkbenchContribution { return; } - const skipVersion = storageService.get(SKIP_VERSION_KEY, StorageScope.GLOBAL, ''); + const skipVersion = storageService.get(SKIP_VERSION_KEY, StorageScope.APPLICATION, ''); if (skipVersion) { return; } const date = new Date().toDateString(); - const lastSessionDate = storageService.get(LAST_SESSION_DATE_KEY, StorageScope.GLOBAL, new Date(0).toDateString()); + const lastSessionDate = storageService.get(LAST_SESSION_DATE_KEY, StorageScope.APPLICATION, new Date(0).toDateString()); if (date === lastSessionDate) { return; } - const sessionCount = (storageService.getNumber(SESSION_COUNT_KEY, StorageScope.GLOBAL, 0) || 0) + 1; - storageService.store(LAST_SESSION_DATE_KEY, date, StorageScope.GLOBAL, StorageTarget.USER); - storageService.store(SESSION_COUNT_KEY, sessionCount, StorageScope.GLOBAL, StorageTarget.USER); + const sessionCount = (storageService.getNumber(SESSION_COUNT_KEY, StorageScope.APPLICATION, 0) || 0) + 1; + storageService.store(LAST_SESSION_DATE_KEY, date, StorageScope.APPLICATION, StorageTarget.USER); + storageService.store(SESSION_COUNT_KEY, sessionCount, StorageScope.APPLICATION, StorageTarget.USER); if (sessionCount < 9) { return; } - const isCandidate = storageService.getBoolean(IS_CANDIDATE_KEY, StorageScope.GLOBAL, false) + const isCandidate = storageService.getBoolean(IS_CANDIDATE_KEY, StorageScope.APPLICATION, false) || Math.random() < PROBABILITY; - storageService.store(IS_CANDIDATE_KEY, isCandidate, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(IS_CANDIDATE_KEY, isCandidate, StorageScope.APPLICATION, StorageTarget.USER); if (!isCandidate) { - storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.APPLICATION, StorageTarget.USER); return; } @@ -73,18 +73,18 @@ class NPSContribution implements IWorkbenchContribution { run: () => { telemetryService.getTelemetryInfo().then(info => { openerService.open(URI.parse(`${productService.npsSurveyUrl}?o=${encodeURIComponent(platform)}&v=${encodeURIComponent(productService.version)}&m=${encodeURIComponent(info.machineId)}`)); - storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER); - storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(IS_CANDIDATE_KEY, false, StorageScope.APPLICATION, StorageTarget.USER); + storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.APPLICATION, StorageTarget.USER); }); } }, { label: nls.localize('remindLater', "Remind Me later"), - run: () => storageService.store(SESSION_COUNT_KEY, sessionCount - 3, StorageScope.GLOBAL, StorageTarget.USER) + run: () => storageService.store(SESSION_COUNT_KEY, sessionCount - 3, StorageScope.APPLICATION, StorageTarget.USER) }, { label: nls.localize('neverAgain', "Don't Show Again"), run: () => { - storageService.store(IS_CANDIDATE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER); - storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(IS_CANDIDATE_KEY, false, StorageScope.APPLICATION, StorageTarget.USER); + storageService.store(SKIP_VERSION_KEY, productService.version, StorageScope.APPLICATION, StorageTarget.USER); } }], { sticky: true } diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts index 8e263a87401..ab86a14c069 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts @@ -67,7 +67,7 @@ export class WorkspaceTags implements IWorkbenchContribution { value = 'Unknown'; } - this.telemetryService.publicLog2<{ edition: string }, { edition: { classification: 'SystemMetaData'; purpose: 'BusinessInsight' } }>('windowsEdition', { edition: value }); + this.telemetryService.publicLog2<{ edition: string }, { owner: 'sbatten'; edition: { classification: 'SystemMetaData'; purpose: 'BusinessInsight' } }>('windowsEdition', { edition: value }); } private async getWorkspaceInformation(): Promise<IWorkspaceInformation> { @@ -89,6 +89,7 @@ export class WorkspaceTags implements IWorkbenchContribution { private reportWorkspaceTags(tags: Tags): void { /* __GDPR__ "workspce.tags" : { + "owner": "lramos15", "${include}": [ "${WorkspaceTags}" ] @@ -116,6 +117,7 @@ export class WorkspaceTags implements IWorkbenchContribution { set.forEach(item => list.push(item)); /* __GDPR__ "workspace.remotes" : { + "owner": "lramos15", "domains" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -192,6 +194,7 @@ export class WorkspaceTags implements IWorkbenchContribution { if (Object.keys(tags).length) { /* __GDPR__ "workspace.azure" : { + "owner": "lramos15", "${include}": [ "${AzureTags}" ] diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index e5690557689..dd0ef223db9 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -687,7 +687,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { const requirementsTxtPromises = getFilePromises('requirements.txt', this.fileService, this.textFileService, content => { const dependencies: string[] = splitLines(content.value); - for (let dependency of dependencies) { + for (const dependency of dependencies) { // Dependencies in requirements.txt can have 3 formats: `foo==3.1, foo>=3.1, foo` const format1 = dependency.split('=='); const format2 = dependency.split('>='); @@ -702,7 +702,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { // We're only interested in the '[packages]' section of the Pipfile dependencies = dependencies.slice(dependencies.indexOf('[packages]') + 1); - for (let dependency of dependencies) { + for (const dependency of dependencies) { if (dependency.trim().indexOf('[') > -1) { break; } @@ -719,9 +719,9 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { const packageJsonPromises = getFilePromises('package.json', this.fileService, this.textFileService, content => { try { const packageJsonContents = JSON.parse(content.value); - let dependencies = Object.keys(packageJsonContents['dependencies'] || {}).concat(Object.keys(packageJsonContents['devDependencies'] || {})); + const dependencies = Object.keys(packageJsonContents['dependencies'] || {}).concat(Object.keys(packageJsonContents['devDependencies'] || {})); - for (let dependency of dependencies) { + for (const dependency of dependencies) { if (dependency.startsWith('react-native')) { tags['workspace.reactNative'] = true; } else if ('tns-core-modules' === dependency || '@nativescript/core' === dependency) { diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 7e4c505c69c..9ef8982eebe 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -26,7 +26,7 @@ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configur import { IFileService, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { ProblemMatcherRegistry, NamedProblemMatcher } from 'vs/workbench/contrib/tasks/common/problemMatcher'; +import { ProblemMatcherRegistry, INamedProblemMatcher } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IProgressService, IProgressOptions, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -36,7 +36,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IModelService } from 'vs/editor/common/services/model'; -import Constants from 'vs/workbench/contrib/markers/browser/constants'; +import { Markers } from 'vs/workbench/contrib/markers/common/markers'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder, IWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -47,14 +47,15 @@ import { IOutputService, IOutputChannel } from 'vs/workbench/services/output/com import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, TaskErrors, TaskTerminateResponse, TaskSystemInfo, ITaskExecuteResult } from 'vs/workbench/contrib/tasks/common/taskSystem'; +import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, TaskErrors, ITaskTerminateResponse, ITaskSystemInfo, ITaskExecuteResult } from 'vs/workbench/contrib/tasks/common/taskSystem'; import { - Task, CustomTask, ConfiguringTask, ContributedTask, InMemoryTask, TaskEvent, - TaskSet, TaskGroup, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, - TaskSorter, TaskIdentifier, KeyedTaskIdentifier, TASK_RUNNING_STATE, TaskRunSource, - KeyedTaskIdentifier as NKeyedTaskIdentifier, TaskDefinition, RuntimeType + Task, CustomTask, ConfiguringTask, ContributedTask, InMemoryTask, ITaskEvent, + ITaskSet, TaskGroup, ExecutionEngine, JsonSchemaVersion, TaskSourceKind, + TaskSorter, ITaskIdentifier, TASK_RUNNING_STATE, TaskRunSource, + KeyedTaskIdentifier as KeyedTaskIdentifier, TaskDefinition, RuntimeType, + USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/tasks'; -import { ITaskService, ITaskProvider, ProblemMatcherRunOptions, CustomizationProperties, TaskFilter, WorkspaceFolderTaskResult, USER_TASKS_GROUP_KEY, CustomExecutionSupportedContext, ShellExecutionSupportedContext, ProcessExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; +import { ITaskService, ITaskProvider, IProblemMatcherRunOptions, ICustomizationProperties, ITaskFilter, IWorkspaceFolderTaskResult, CustomExecutionSupportedContext, ShellExecutionSupportedContext, ProcessExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; import { getTemplates as getTaskTemplates } from 'vs/workbench/contrib/tasks/common/taskTemplates'; import * as TaskConfig from '../common/taskConfiguration'; @@ -75,10 +76,10 @@ import { ITextEditorSelection, TextEditorSelectionRevealType } from 'vs/platform import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; -import { isWorkspaceFolder, TaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, TaskQuickPick, QUICKOPEN_SKIP_CONFIG, configureTaskIcon } from 'vs/workbench/contrib/tasks/browser/taskQuickPick'; +import { isWorkspaceFolder, ITaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, TaskQuickPick, QUICKOPEN_SKIP_CONFIG, configureTaskIcon } from 'vs/workbench/contrib/tasks/browser/taskQuickPick'; import { ILogService } from 'vs/platform/log/common/log'; import { once } from 'vs/base/common/functional'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { VirtualWorkspaceContext } from 'vs/workbench/common/contextkeys'; import { Schemas } from 'vs/base/common/network'; @@ -93,7 +94,7 @@ export namespace ConfigureTaskAction { export const TEXT = nls.localize('ConfigureTaskRunnerAction.label', "Configure Task"); } -type TaskQuickPickEntryType = (IQuickPickItem & { task: Task }) | (IQuickPickItem & { folder: IWorkspaceFolder }) | (IQuickPickItem & { settingType: string }); +export type TaskQuickPickEntryType = (IQuickPickItem & { task: Task }) | (IQuickPickItem & { folder: IWorkspaceFolder }) | (IQuickPickItem & { settingType: string }); class ProblemReporter implements TaskConfig.IProblemReporter { @@ -128,13 +129,13 @@ class ProblemReporter implements TaskConfig.IProblemReporter { } } -export interface WorkspaceFolderConfigurationResult { +export interface IWorkspaceFolderConfigurationResult { workspaceFolder: IWorkspaceFolder; - config: TaskConfig.ExternalTaskRunnerConfiguration | undefined; + config: TaskConfig.IExternalTaskRunnerConfiguration | undefined; hasErrors: boolean; } -interface CommandUpgrade { +interface ICommandUpgrade { command?: string; args?: string[]; } @@ -178,7 +179,7 @@ class TaskMap { } public all(): Task[] { - let result: Task[] = []; + const result: Task[] = []; this._store.forEach((values) => result.push(...values)); return result; } @@ -195,7 +196,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer public static OutputChannelId: string = 'tasks'; public static OutputChannelLabel: string = nls.localize('tasks', "Tasks"); - private static nextHandle: number = 0; + private static _nextHandle: number = 0; private _schemaVersion: JsonSchemaVersion | undefined; private _executionEngine: ExecutionEngine | undefined; @@ -205,9 +206,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private _showIgnoreMessage?: boolean; private _providers: Map<number, ITaskProvider>; private _providerTypes: Map<number, string>; - protected _taskSystemInfos: Map<string, TaskSystemInfo[]>; + protected _taskSystemInfos: Map<string, ITaskSystemInfo[]>; - protected _workspaceTasksPromise?: Promise<Map<string, WorkspaceFolderTaskResult>>; + protected _workspaceTasksPromise?: Promise<Map<string, IWorkspaceFolderTaskResult>>; protected _taskSystem?: ITaskSystem; protected _taskSystemListener?: IDisposable; @@ -217,65 +218,66 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected _taskRunningState: IContextKey<boolean>; protected _outputChannel: IOutputChannel; - protected readonly _onDidStateChange: Emitter<TaskEvent>; + protected readonly _onDidStateChange: Emitter<ITaskEvent>; private _waitForSupportedExecutions: Promise<void>; private _onDidRegisterSupportedExecutions: Emitter<void> = new Emitter(); private _onDidChangeTaskSystemInfo: Emitter<void> = new Emitter(); public onDidChangeTaskSystemInfo: Event<void> = this._onDidChangeTaskSystemInfo.event; constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @IMarkerService protected readonly markerService: IMarkerService, - @IOutputService protected readonly outputService: IOutputService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, - @IViewsService private readonly viewsService: IViewsService, - @ICommandService private readonly commandService: ICommandService, - @IEditorService private readonly editorService: IEditorService, - @IFileService protected readonly fileService: IFileService, - @IWorkspaceContextService protected readonly contextService: IWorkspaceContextService, - @ITelemetryService protected readonly telemetryService: ITelemetryService, - @ITextFileService private readonly textFileService: ITextFileService, - @IModelService protected readonly modelService: IModelService, - @IExtensionService private readonly extensionService: IExtensionService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IConfigurationResolverService protected readonly configurationResolverService: IConfigurationResolverService, - @ITerminalService private readonly terminalService: ITerminalService, - @ITerminalGroupService private readonly terminalGroupService: ITerminalGroupService, - @IStorageService private readonly storageService: IStorageService, - @IProgressService private readonly progressService: IProgressService, - @IOpenerService private readonly openerService: IOpenerService, - @IDialogService protected readonly dialogService: IDialogService, - @INotificationService private readonly notificationService: INotificationService, - @IContextKeyService protected readonly contextKeyService: IContextKeyService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @ITerminalProfileResolverService private readonly terminalProfileResolverService: ITerminalProfileResolverService, - @IPathService private readonly pathService: IPathService, - @ITextModelService private readonly textModelResolverService: ITextModelService, - @IPreferencesService private readonly preferencesService: IPreferencesService, - @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, - @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @ILogService private readonly logService: ILogService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IMarkerService protected readonly _markerService: IMarkerService, + @IOutputService protected readonly _outputService: IOutputService, + @IPaneCompositePartService private readonly _paneCompositeService: IPaneCompositePartService, + @IViewsService private readonly _viewsService: IViewsService, + @ICommandService private readonly _commandService: ICommandService, + @IEditorService private readonly _editorService: IEditorService, + @IFileService protected readonly _fileService: IFileService, + @IWorkspaceContextService protected readonly _contextService: IWorkspaceContextService, + @ITelemetryService protected readonly _telemetryService: ITelemetryService, + @ITextFileService private readonly _textFileService: ITextFileService, + @IModelService protected readonly _modelService: IModelService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IConfigurationResolverService protected readonly _configurationResolverService: IConfigurationResolverService, + @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, + @IStorageService private readonly _storageService: IStorageService, + @IProgressService private readonly _progressService: IProgressService, + @IOpenerService private readonly _openerService: IOpenerService, + @IDialogService protected readonly _dialogService: IDialogService, + @INotificationService private readonly _notificationService: INotificationService, + @IContextKeyService protected readonly _contextKeyService: IContextKeyService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, + @IPathService private readonly _pathService: IPathService, + @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IPreferencesService private readonly _preferencesService: IPreferencesService, + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, + @IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, + @ILogService private readonly _logService: ILogService, + @IThemeService private readonly _themeService: IThemeService ) { super(); this._workspaceTasksPromise = undefined; this._taskSystem = undefined; this._taskSystemListener = undefined; - this._outputChannel = this.outputService.getChannel(AbstractTaskService.OutputChannelId)!; + this._outputChannel = this._outputService.getChannel(AbstractTaskService.OutputChannelId)!; this._providers = new Map<number, ITaskProvider>(); this._providerTypes = new Map<number, string>(); - this._taskSystemInfos = new Map<string, TaskSystemInfo[]>(); - this._register(this.contextService.onDidChangeWorkspaceFolders(() => { - let folderSetup = this.computeWorkspaceFolderSetup(); + this._taskSystemInfos = new Map<string, ITaskSystemInfo[]>(); + this._register(this._contextService.onDidChangeWorkspaceFolders(() => { + const folderSetup = this._computeWorkspaceFolderSetup(); if (this.executionEngine !== folderSetup[2]) { - this.disposeTaskSystemListeners(); + this._disposeTaskSystemListeners(); this._taskSystem = undefined; } - this.updateSetup(folderSetup); - return this.updateWorkspaceTasks(TaskRunSource.FolderOpen); + this._updateSetup(folderSetup); + return this._updateWorkspaceTasks(TaskRunSource.FolderOpen); })); - this._register(this.configurationService.onDidChangeConfiguration(() => { + this._register(this._configurationService.onDidChangeConfiguration(() => { if (!this._taskSystem && !this._workspaceTasksPromise) { return; } @@ -283,16 +285,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._outputChannel.clear(); } - this.setTaskLRUCacheLimit(); - return this.updateWorkspaceTasks(TaskRunSource.ConfigurationChange); + this._setTaskLRUCacheLimit(); + return this._updateWorkspaceTasks(TaskRunSource.ConfigurationChange); })); - this._taskRunningState = TASK_RUNNING_STATE.bindTo(contextKeyService); + this._taskRunningState = TASK_RUNNING_STATE.bindTo(_contextKeyService); this._onDidStateChange = this._register(new Emitter()); - this.registerCommands(); - this.configurationResolverService.contributeVariable('defaultBuildTask', async (): Promise<string | undefined> => { - let tasks = await this.getTasksForGroup(TaskGroup.Build); + this._registerCommands(); + this._configurationResolverService.contributeVariable('defaultBuildTask', async (): Promise<string | undefined> => { + let tasks = await this._getTasksForGroup(TaskGroup.Build); if (tasks.length > 0) { - let { none, defaults } = this.splitPerGroupType(tasks); + const { none, defaults } = this._splitPerGroupType(tasks); if (defaults.length === 1) { return defaults[0]._label; } else if (defaults.length + none.length > 0) { @@ -300,12 +302,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - let entry: TaskQuickPickEntry | null | undefined; + let entry: ITaskQuickPickEntry | null | undefined; if (tasks && tasks.length > 0) { - entry = await this.showQuickPick(tasks, nls.localize('TaskService.pickBuildTaskForLabel', 'Select the build task (there is no default build task defined)')); + entry = await this._showQuickPick(tasks, nls.localize('TaskService.pickBuildTaskForLabel', 'Select the build task (there is no default build task defined)')); } - let task: Task | undefined | null = entry ? entry.task : undefined; + const task: Task | undefined | null = entry ? entry.task : undefined; if (!task) { return undefined; } @@ -315,27 +317,27 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._waitForSupportedExecutions = new Promise(resolve => { once(this._onDidRegisterSupportedExecutions.event)(() => resolve()); }); - this.upgrade(); + this._upgrade(); } public registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean) { if (custom !== undefined) { - const customContext = CustomExecutionSupportedContext.bindTo(this.contextKeyService); + const customContext = CustomExecutionSupportedContext.bindTo(this._contextKeyService); customContext.set(custom); } - const isVirtual = !!VirtualWorkspaceContext.getValue(this.contextKeyService); + const isVirtual = !!VirtualWorkspaceContext.getValue(this._contextKeyService); if (shell !== undefined) { - const shellContext = ShellExecutionSupportedContext.bindTo(this.contextKeyService); + const shellContext = ShellExecutionSupportedContext.bindTo(this._contextKeyService); shellContext.set(shell && !isVirtual); } if (process !== undefined) { - const processContext = ProcessExecutionSupportedContext.bindTo(this.contextKeyService); + const processContext = ProcessExecutionSupportedContext.bindTo(this._contextKeyService); processContext.set(process && !isVirtual); } this._onDidRegisterSupportedExecutions.fire(); } - public get onDidStateChange(): Event<TaskEvent> { + public get onDidStateChange(): Event<ITaskEvent> { return this._onDidStateChange.event; } @@ -343,12 +345,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return this.inTerminal(); } - private registerCommands(): void { + private _registerCommands(): void { CommandsRegistry.registerCommand({ id: 'workbench.action.tasks.runTask', handler: async (accessor, arg) => { - if (await this.trust()) { - this.runTaskCommand(arg); + if (await this._trust()) { + this._runTaskCommand(arg); } }, description: { @@ -363,120 +365,120 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); CommandsRegistry.registerCommand('workbench.action.tasks.reRunTask', async (accessor, arg) => { - if (await this.trust()) { - this.reRunTaskCommand(); + if (await this._trust()) { + this._reRunTaskCommand(); } }); CommandsRegistry.registerCommand('workbench.action.tasks.restartTask', async (accessor, arg) => { - if (await this.trust()) { - this.runRestartTaskCommand(arg); + if (await this._trust()) { + this._runRestartTaskCommand(arg); } }); CommandsRegistry.registerCommand('workbench.action.tasks.terminate', async (accessor, arg) => { - if (await this.trust()) { - this.runTerminateCommand(arg); + if (await this._trust()) { + this._runTerminateCommand(arg); } }); CommandsRegistry.registerCommand('workbench.action.tasks.showLog', () => { - if (!this.canRunCommand()) { + if (!this._canRunCommand()) { return; } - this.showOutput(); + this._showOutput(); }); CommandsRegistry.registerCommand('workbench.action.tasks.build', async () => { - if (!this.canRunCommand()) { + if (!this._canRunCommand()) { return; } - if (await this.trust()) { - this.runBuildCommand(); + if (await this._trust()) { + this._runBuildCommand(); } }); CommandsRegistry.registerCommand('workbench.action.tasks.test', async () => { - if (!this.canRunCommand()) { + if (!this._canRunCommand()) { return; } - if (await this.trust()) { - this.runTestCommand(); + if (await this._trust()) { + this._runTestCommand(); } }); CommandsRegistry.registerCommand('workbench.action.tasks.configureTaskRunner', async () => { - if (await this.trust()) { - this.runConfigureTasks(); + if (await this._trust()) { + this._runConfigureTasks(); } }); CommandsRegistry.registerCommand('workbench.action.tasks.configureDefaultBuildTask', async () => { - if (await this.trust()) { - this.runConfigureDefaultBuildTask(); + if (await this._trust()) { + this._runConfigureDefaultBuildTask(); } }); CommandsRegistry.registerCommand('workbench.action.tasks.configureDefaultTestTask', async () => { - if (await this.trust()) { - this.runConfigureDefaultTestTask(); + if (await this._trust()) { + this._runConfigureDefaultTestTask(); } }); CommandsRegistry.registerCommand('workbench.action.tasks.showTasks', async () => { - if (await this.trust()) { + if (await this._trust()) { return this.runShowTasks(); } }); - CommandsRegistry.registerCommand('workbench.action.tasks.toggleProblems', () => this.commandService.executeCommand(Constants.TOGGLE_MARKERS_VIEW_ACTION_ID)); + CommandsRegistry.registerCommand('workbench.action.tasks.toggleProblems', () => this._commandService.executeCommand(Markers.TOGGLE_MARKERS_VIEW_ACTION_ID)); CommandsRegistry.registerCommand('workbench.action.tasks.openUserTasks', async () => { - const resource = this.getResourceForKind(TaskSourceKind.User); + const resource = this._getResourceForKind(TaskSourceKind.User); if (resource) { - this.openTaskFile(resource, TaskSourceKind.User); + this._openTaskFile(resource, TaskSourceKind.User); } }); CommandsRegistry.registerCommand('workbench.action.tasks.openWorkspaceFileTasks', async () => { - const resource = this.getResourceForKind(TaskSourceKind.WorkspaceFile); + const resource = this._getResourceForKind(TaskSourceKind.WorkspaceFile); if (resource) { - this.openTaskFile(resource, TaskSourceKind.WorkspaceFile); + this._openTaskFile(resource, TaskSourceKind.WorkspaceFile); } }); } private get workspaceFolders(): IWorkspaceFolder[] { if (!this._workspaceFolders) { - this.updateSetup(); + this._updateSetup(); } return this._workspaceFolders!; } private get ignoredWorkspaceFolders(): IWorkspaceFolder[] { if (!this._ignoredWorkspaceFolders) { - this.updateSetup(); + this._updateSetup(); } return this._ignoredWorkspaceFolders!; } protected get executionEngine(): ExecutionEngine { if (this._executionEngine === undefined) { - this.updateSetup(); + this._updateSetup(); } return this._executionEngine!; } private get schemaVersion(): JsonSchemaVersion { if (this._schemaVersion === undefined) { - this.updateSetup(); + this._updateSetup(); } return this._schemaVersion!; } private get showIgnoreMessage(): boolean { if (this._showIgnoreMessage === undefined) { - this._showIgnoreMessage = !this.storageService.getBoolean(AbstractTaskService.IgnoreTask010DonotShowAgain_key, StorageScope.WORKSPACE, false); + this._showIgnoreMessage = !this._storageService.getBoolean(AbstractTaskService.IgnoreTask010DonotShowAgain_key, StorageScope.WORKSPACE, false); } return this._showIgnoreMessage; } @@ -499,25 +501,25 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private async _activateTaskProviders(type: string | undefined): Promise<void> { // We need to first wait for extensions to be registered because we might read // the `TaskDefinitionRegistry` in case `type` is `undefined` - await this.extensionService.whenInstalledExtensionsRegistered(); + await this._extensionService.whenInstalledExtensionsRegistered(); await Promise.all( - this._getActivationEvents(type).map(activationEvent => this.extensionService.activateByEvent(activationEvent)) + this._getActivationEvents(type).map(activationEvent => this._extensionService.activateByEvent(activationEvent)) ); } - private updateSetup(setup?: [IWorkspaceFolder[], IWorkspaceFolder[], ExecutionEngine, JsonSchemaVersion, IWorkspace | undefined]): void { + private _updateSetup(setup?: [IWorkspaceFolder[], IWorkspaceFolder[], ExecutionEngine, JsonSchemaVersion, IWorkspace | undefined]): void { if (!setup) { - setup = this.computeWorkspaceFolderSetup(); + setup = this._computeWorkspaceFolderSetup(); } this._workspaceFolders = setup[0]; if (this._ignoredWorkspaceFolders) { if (this._ignoredWorkspaceFolders.length !== setup[1].length) { this._showIgnoreMessage = undefined; } else { - let set: Set<string> = new Set(); + const set: Set<string> = new Set(); this._ignoredWorkspaceFolders.forEach(folder => set.add(folder.uri.toString())); - for (let folder of setup[1]) { + for (const folder of setup[1]) { if (!set.has(folder.uri.toString())) { this._showIgnoreMessage = undefined; break; @@ -531,19 +533,19 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._workspace = setup[4]; } - protected showOutput(runSource: TaskRunSource = TaskRunSource.User): void { - if (!VirtualWorkspaceContext.getValue(this.contextKeyService) && ((runSource === TaskRunSource.User) || (runSource === TaskRunSource.ConfigurationChange))) { - this.notificationService.prompt(Severity.Warning, nls.localize('taskServiceOutputPrompt', 'There are task errors. See the output for details.'), + protected _showOutput(runSource: TaskRunSource = TaskRunSource.User): void { + if (!VirtualWorkspaceContext.getValue(this._contextKeyService) && ((runSource === TaskRunSource.User) || (runSource === TaskRunSource.ConfigurationChange))) { + this._notificationService.prompt(Severity.Warning, nls.localize('taskServiceOutputPrompt', 'There are task errors. See the output for details.'), [{ label: nls.localize('showOutput', "Show output"), run: () => { - this.outputService.showChannel(this._outputChannel.id, true); + this._outputService.showChannel(this._outputChannel.id, true); } }]); } } - protected disposeTaskSystemListeners(): void { + protected _disposeTaskSystemListeners(): void { if (this._taskSystemListener) { this._taskSystemListener.dispose(); } @@ -555,7 +557,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer dispose: () => { } }; } - let handle = AbstractTaskService.nextHandle++; + const handle = AbstractTaskService._nextHandle++; this._providers.set(handle, provider); this._providerTypes.set(handle, type); return { @@ -567,16 +569,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } get hasTaskSystemInfo(): boolean { - let infosCount = Array.from(this._taskSystemInfos.values()).flat().length; + const infosCount = Array.from(this._taskSystemInfos.values()).flat().length; // If there's a remoteAuthority, then we end up with 2 taskSystemInfos, // one for each extension host. - if (this.environmentService.remoteAuthority) { + if (this._environmentService.remoteAuthority) { return infosCount > 1; } return infosCount > 0; } - public registerTaskSystem(key: string, info: TaskSystemInfo): void { + public registerTaskSystem(key: string, info: ITaskSystemInfo): void { // Ideally the Web caller of registerRegisterTaskSystem would use the correct key. // However, the caller doesn't know about the workspace folders at the time of the call, even though we know about them here. if (info.platform === Platform.Platform.Web) { @@ -599,7 +601,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private getTaskSystemInfo(key: string): TaskSystemInfo | undefined { + private _getTaskSystemInfo(key: string): ITaskSystemInfo | undefined { const infos = this._taskSystemInfos.get(key); return (infos && infos.length) ? infos[0] : undefined; } @@ -649,8 +651,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - public async getTask(folder: IWorkspace | IWorkspaceFolder | string, identifier: string | TaskIdentifier, compareId: boolean = false): Promise<Task | undefined> { - if (!(await this.trust())) { + public async getTask(folder: IWorkspace | IWorkspaceFolder | string, identifier: string | ITaskIdentifier, compareId: boolean = false): Promise<Task | undefined> { + if (!(await this._trust())) { return; } const name = Types.isString(folder) ? folder : isWorkspaceFolder(folder) ? folder.name : folder.configuration ? resources.basename(folder.configuration) : undefined; @@ -686,7 +688,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } // We didn't find the task, so we need to ask all resolvers about it - return this.getGroupedTasks().then((map) => { + return this._getGroupedTasks().then((map) => { let values = map.get(folder); values = values.concat(map.get(USER_TASKS_GROUP_KEY)); @@ -699,7 +701,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } public async tryResolveTask(configuringTask: ConfiguringTask): Promise<Task | undefined> { - if (!(await this.trust())) { + if (!(await this._trust())) { return; } await this._activateTaskProviders(configuringTask.type); @@ -708,7 +710,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer for (const [handle, provider] of this._providers) { const providerType = this._providerTypes.get(handle); if (configuringTask.type === providerType) { - if (providerType && !this.isTaskProviderEnabled(providerType)) { + if (providerType && !this._isTaskProviderEnabled(providerType)) { matchingProviderUnavailable = true; continue; } @@ -749,29 +751,29 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return; } - protected abstract versionAndEngineCompatible(filter?: TaskFilter): boolean; + protected abstract _versionAndEngineCompatible(filter?: ITaskFilter): boolean; - public async tasks(filter?: TaskFilter): Promise<Task[]> { - if (!(await this.trust())) { + public async tasks(filter?: ITaskFilter): Promise<Task[]> { + if (!(await this._trust())) { return []; } - if (!this.versionAndEngineCompatible(filter)) { + if (!this._versionAndEngineCompatible(filter)) { return Promise.resolve<Task[]>([]); } - return this.getGroupedTasks(filter ? filter.type : undefined).then((map) => { + return this._getGroupedTasks(filter ? filter.type : undefined).then((map) => { if (!filter || !filter.type) { return map.all(); } - let result: Task[] = []; + const result: Task[] = []; map.forEach((tasks) => { - for (let task of tasks) { + for (const task of tasks) { if (ContributedTask.is(task) && ((task.defines.type === filter.type) || (task._source.label === filter.type))) { result.push(task); } else if (CustomTask.is(task)) { if (task.type === filter.type) { result.push(task); } else { - let customizes = task.customizes(); + const customizes = task.customizes(); if (customizes && customizes.type === filter.type) { result.push(task); } @@ -785,9 +787,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer public taskTypes(): string[] { const types: string[] = []; - if (this.isProvideTasksEnabled()) { + if (this._isProvideTasksEnabled()) { for (const definition of TaskDefinitionRegistry.all()) { - if (this.isTaskProviderEnabled(definition.taskType)) { + if (this._isTaskProviderEnabled(definition.taskType)) { types.push(definition.taskType); } } @@ -796,10 +798,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } public createSorter(): TaskSorter { - return new TaskSorter(this.contextService.getWorkspace() ? this.contextService.getWorkspace().folders : []); + return new TaskSorter(this._contextService.getWorkspace() ? this._contextService.getWorkspace().folders : []); } - private isActive(): Promise<boolean> { + private _isActive(): Promise<boolean> { if (!this._taskSystem) { return Promise.resolve(false); } @@ -824,15 +826,15 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (this._recentlyUsedTasksV1) { return this._recentlyUsedTasksV1; } - const quickOpenHistoryLimit = this.configurationService.getValue<number>(QUICKOPEN_HISTORY_LIMIT_CONFIG); + const quickOpenHistoryLimit = this._configurationService.getValue<number>(QUICKOPEN_HISTORY_LIMIT_CONFIG); this._recentlyUsedTasksV1 = new LRUCache<string, string>(quickOpenHistoryLimit); - let storageValue = this.storageService.get(AbstractTaskService.RecentlyUsedTasks_Key, StorageScope.WORKSPACE); + const storageValue = this._storageService.get(AbstractTaskService.RecentlyUsedTasks_Key, StorageScope.WORKSPACE); if (storageValue) { try { - let values: string[] = JSON.parse(storageValue); + const values: string[] = JSON.parse(storageValue); if (Array.isArray(values)) { - for (let value of values) { + for (const value of values) { this._recentlyUsedTasksV1.set(value, value); } } @@ -843,19 +845,19 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return this._recentlyUsedTasksV1; } - private getRecentlyUsedTasks(): LRUCache<string, string> { + private _getRecentlyUsedTasks(): LRUCache<string, string> { if (this._recentlyUsedTasks) { return this._recentlyUsedTasks; } - const quickOpenHistoryLimit = this.configurationService.getValue<number>(QUICKOPEN_HISTORY_LIMIT_CONFIG); + const quickOpenHistoryLimit = this._configurationService.getValue<number>(QUICKOPEN_HISTORY_LIMIT_CONFIG); this._recentlyUsedTasks = new LRUCache<string, string>(quickOpenHistoryLimit); - let storageValue = this.storageService.get(AbstractTaskService.RecentlyUsedTasks_KeyV2, StorageScope.WORKSPACE); + const storageValue = this._storageService.get(AbstractTaskService.RecentlyUsedTasks_KeyV2, StorageScope.WORKSPACE); if (storageValue) { try { - let values: [string, string][] = JSON.parse(storageValue); + const values: [string, string][] = JSON.parse(storageValue); if (Array.isArray(values)) { - for (let value of values) { + for (const value of values) { this._recentlyUsedTasks.set(value[0], value[1]); } } @@ -866,7 +868,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return this._recentlyUsedTasks; } - private getFolderFromTaskKey(key: string): { folder: string | undefined; isWorkspaceFile: boolean | undefined } { + private _getFolderFromTaskKey(key: string): { folder: string | undefined; isWorkspaceFile: boolean | undefined } { const keyValue: { folder: string | undefined; id: string | undefined } = JSON.parse(key); return { folder: keyValue.folder, isWorkspaceFile: keyValue.id?.endsWith(TaskSourceKind.WorkspaceFile) @@ -880,7 +882,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); const folderToTasksMap: Map<string, any> = new Map(); const workspaceToTaskMap: Map<string, any> = new Map(); - const recentlyUsedTasks = this.getRecentlyUsedTasks(); + const recentlyUsedTasks = this._getRecentlyUsedTasks(); const tasks: (Task | ConfiguringTask)[] = []; function addTaskToMap(map: Map<string, any>, folder: string | undefined, task: any) { @@ -895,20 +897,20 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer for (const entry of recentlyUsedTasks.entries()) { const key = entry[0]; const task = JSON.parse(entry[1]); - const folderInfo = this.getFolderFromTaskKey(key); + const folderInfo = this._getFolderFromTaskKey(key); addTaskToMap(folderInfo.isWorkspaceFile ? workspaceToTaskMap : folderToTasksMap, folderInfo.folder, task); } const readTasksMap: Map<string, (Task | ConfiguringTask)> = new Map(); async function readTasks(that: AbstractTaskService, map: Map<string, any>, isWorkspaceFile: boolean) { for (const key of map.keys()) { - let custom: CustomTask[] = []; - let customized: IStringDictionary<ConfiguringTask> = Object.create(null); + const custom: CustomTask[] = []; + const customized: IStringDictionary<ConfiguringTask> = Object.create(null); const taskConfigSource = (folderMap[key] ? (isWorkspaceFile ? TaskConfig.TaskConfigSource.WorkspaceFile : TaskConfig.TaskConfigSource.TasksJson) : TaskConfig.TaskConfigSource.User); - await that.computeTasksForSingleConfig(folderMap[key] ?? await that.getAFolder(), { + await that._computeTasksForSingleConfig(folderMap[key] ?? await that._getAFolder(), { version: '2.0.0', tasks: map.get(key) }, TaskRunSource.System, custom, customized, taskConfigSource, true); @@ -938,27 +940,27 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } public removeRecentlyUsedTask(taskRecentlyUsedKey: string) { - if (this.getRecentlyUsedTasks().has(taskRecentlyUsedKey)) { - this.getRecentlyUsedTasks().delete(taskRecentlyUsedKey); - this.saveRecentlyUsedTasks(); + if (this._getRecentlyUsedTasks().has(taskRecentlyUsedKey)) { + this._getRecentlyUsedTasks().delete(taskRecentlyUsedKey); + this._saveRecentlyUsedTasks(); } } - private setTaskLRUCacheLimit() { - const quickOpenHistoryLimit = this.configurationService.getValue<number>(QUICKOPEN_HISTORY_LIMIT_CONFIG); + private _setTaskLRUCacheLimit() { + const quickOpenHistoryLimit = this._configurationService.getValue<number>(QUICKOPEN_HISTORY_LIMIT_CONFIG); if (this._recentlyUsedTasks) { this._recentlyUsedTasks.limit = quickOpenHistoryLimit; } } - private async setRecentlyUsedTask(task: Task): Promise<void> { + private async _setRecentlyUsedTask(task: Task): Promise<void> { let key = task.getRecentlyUsedKey(); if (!InMemoryTask.is(task) && key) { - const customizations = this.createCustomizableTask(task); + const customizations = this._createCustomizableTask(task); if (ContributedTask.is(task) && customizations) { - let custom: CustomTask[] = []; - let customized: IStringDictionary<ConfiguringTask> = Object.create(null); - await this.computeTasksForSingleConfig(task._source.workspaceFolder ?? this.workspaceFolders[0], { + const custom: CustomTask[] = []; + const customized: IStringDictionary<ConfiguringTask> = Object.create(null); + await this._computeTasksForSingleConfig(task._source.workspaceFolder ?? this.workspaceFolders[0], { version: '2.0.0', tasks: [customizations] }, TaskRunSource.System, custom, customized, TaskConfig.TaskConfigSource.TasksJson, true); @@ -966,16 +968,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer key = customized[configuration].getRecentlyUsedKey()!; } } - this.getRecentlyUsedTasks().set(key, JSON.stringify(customizations)); - this.saveRecentlyUsedTasks(); + this._getRecentlyUsedTasks().set(key, JSON.stringify(customizations)); + this._saveRecentlyUsedTasks(); } } - private saveRecentlyUsedTasks(): void { + private _saveRecentlyUsedTasks(): void { if (!this._recentlyUsedTasks) { return; } - const quickOpenHistoryLimit = this.configurationService.getValue<number>(QUICKOPEN_HISTORY_LIMIT_CONFIG); + const quickOpenHistoryLimit = this._configurationService.getValue<number>(QUICKOPEN_HISTORY_LIMIT_CONFIG); // setting history limit to 0 means no LRU sorting if (quickOpenHistoryLimit === 0) { return; @@ -988,11 +990,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer for (const key of keys) { keyValues.push([key, this._recentlyUsedTasks.get(key, Touch.None)!]); } - this.storageService.store(AbstractTaskService.RecentlyUsedTasks_KeyV2, JSON.stringify(keyValues), StorageScope.WORKSPACE, StorageTarget.USER); + this._storageService.store(AbstractTaskService.RecentlyUsedTasks_KeyV2, JSON.stringify(keyValues), StorageScope.WORKSPACE, StorageTarget.USER); } - private openDocumentation(): void { - this.openerService.open(URI.parse('https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher')); + private _openDocumentation(): void { + this._openerService.open(URI.parse('https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher')); } private async _findSingleWorkspaceTaskOfGroup(group: TaskGroup): Promise<ITaskSummary | undefined> { @@ -1011,14 +1013,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return undefined; } - private async build(): Promise<ITaskSummary> { + private async _build(): Promise<ITaskSummary> { const tryBuildShortcut = await this._findSingleWorkspaceTaskOfGroup(TaskGroup.Build); if (tryBuildShortcut) { return tryBuildShortcut; } - return this.getGroupedTasks().then((tasks) => { - let runnable = this.createRunnableTask(tasks, TaskGroup.Build); + return this._getGroupedTasks().then((tasks) => { + const runnable = this._createRunnableTask(tasks, TaskGroup.Build); if (!runnable || !runnable.task) { if (this.schemaVersion === JsonSchemaVersion.V0_1_0) { throw new TaskError(Severity.Info, nls.localize('TaskService.noBuildTask1', 'No build task defined. Mark a task with \'isBuildCommand\' in the tasks.json file.'), TaskErrors.NoBuildTask); @@ -1026,21 +1028,21 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer throw new TaskError(Severity.Info, nls.localize('TaskService.noBuildTask2', 'No build task defined. Mark a task with as a \'build\' group in the tasks.json file.'), TaskErrors.NoBuildTask); } } - return this.executeTask(runnable.task, runnable.resolver, TaskRunSource.User); + return this._executeTask(runnable.task, runnable.resolver, TaskRunSource.User); }).then(value => value, (error) => { - this.handleError(error); + this._handleError(error); return Promise.reject(error); }); } - private async runTest(): Promise<ITaskSummary> { + private async _runTest(): Promise<ITaskSummary> { const tryTestShortcut = await this._findSingleWorkspaceTaskOfGroup(TaskGroup.Test); if (tryTestShortcut) { return tryTestShortcut; } - return this.getGroupedTasks().then((tasks) => { - let runnable = this.createRunnableTask(tasks, TaskGroup.Test); + return this._getGroupedTasks().then((tasks) => { + const runnable = this._createRunnableTask(tasks, TaskGroup.Test); if (!runnable || !runnable.task) { if (this.schemaVersion === JsonSchemaVersion.V0_1_0) { throw new TaskError(Severity.Info, nls.localize('TaskService.noTestTask1', 'No test task defined. Mark a task with \'isTestCommand\' in the tasks.json file.'), TaskErrors.NoTestTask); @@ -1048,15 +1050,15 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer throw new TaskError(Severity.Info, nls.localize('TaskService.noTestTask2', 'No test task defined. Mark a task with as a \'test\' group in the tasks.json file.'), TaskErrors.NoTestTask); } } - return this.executeTask(runnable.task, runnable.resolver, TaskRunSource.User); + return this._executeTask(runnable.task, runnable.resolver, TaskRunSource.User); }).then(value => value, (error) => { - this.handleError(error); + this._handleError(error); return Promise.reject(error); }); } - public async run(task: Task | undefined, options?: ProblemMatcherRunOptions, runSource: TaskRunSource = TaskRunSource.System): Promise<ITaskSummary | undefined> { - if (!(await this.trust())) { + public async run(task: Task | undefined, options?: IProblemMatcherRunOptions, runSource: TaskRunSource = TaskRunSource.System): Promise<ITaskSummary | undefined> { + if (!(await this._trust())) { return; } @@ -1065,38 +1067,38 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } return new Promise<ITaskSummary | undefined>((resolve) => { - let resolver = this.createResolver(); - if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(task) && !InMemoryTask.is(task)) { - this.attachProblemMatcher(task).then(toExecute => { + const resolver = this._createResolver(); + if (options && options.attachProblemMatcher && this._shouldAttachProblemMatcher(task) && !InMemoryTask.is(task)) { + this._attachProblemMatcher(task).then(toExecute => { if (toExecute) { - resolve(this.executeTask(toExecute, resolver, runSource)); + resolve(this._executeTask(toExecute, resolver, runSource)); } else { resolve(undefined); } }); } else { - resolve(this.executeTask(task, resolver, runSource)); + resolve(this._executeTask(task, resolver, runSource)); } }).then((value) => { if (runSource === TaskRunSource.User) { this.getWorkspaceTasks().then(workspaceTasks => { - RunAutomaticTasks.promptForPermission(this, this.storageService, this.notificationService, this.workspaceTrustManagementService, this.openerService, workspaceTasks); + RunAutomaticTasks.promptForPermission(this, this._storageService, this._notificationService, this._workspaceTrustManagementService, this._openerService, workspaceTasks); }); } return value; }, (error) => { - this.handleError(error); + this._handleError(error); return Promise.reject(error); }); } - private isProvideTasksEnabled(): boolean { - const settingValue = this.configurationService.getValue('task.autoDetect'); + private _isProvideTasksEnabled(): boolean { + const settingValue = this._configurationService.getValue('task.autoDetect'); return settingValue === 'on'; } - private isProblemMatcherPromptEnabled(type?: string): boolean { - const settingValue = this.configurationService.getValue(PROBLEM_MATCHER_NEVER_CONFIG); + private _isProblemMatcherPromptEnabled(type?: string): boolean { + const settingValue = this._configurationService.getValue(PROBLEM_MATCHER_NEVER_CONFIG); if (Types.isBoolean(settingValue)) { return !settingValue; } @@ -1107,10 +1109,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return !settingValueMap[type]; } - private getTypeForTask(task: Task): string { + private _getTypeForTask(task: Task): string { let type: string; if (CustomTask.is(task)) { - let configProperties: TaskConfig.ConfigurationProperties = task._source.config.element; + const configProperties: TaskConfig.IConfigurationProperties = task._source.config.element; type = (<any>configProperties).type; } else { type = task.getDefinition()!.type; @@ -1118,12 +1120,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return type; } - private shouldAttachProblemMatcher(task: Task): boolean { - const enabled = this.isProblemMatcherPromptEnabled(this.getTypeForTask(task)); + private _shouldAttachProblemMatcher(task: Task): boolean { + const enabled = this._isProblemMatcherPromptEnabled(this._getTypeForTask(task)); if (enabled === false) { return false; } - if (!this.canCustomize(task)) { + if (!this._canCustomize(task)) { return false; } if (task.configurationProperties.group !== undefined && task.configurationProperties.group !== TaskGroup.Build) { @@ -1136,14 +1138,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return !task.hasDefinedMatchers && !!task.configurationProperties.problemMatchers && (task.configurationProperties.problemMatchers.length === 0); } if (CustomTask.is(task)) { - let configProperties: TaskConfig.ConfigurationProperties = task._source.config.element; + const configProperties: TaskConfig.IConfigurationProperties = task._source.config.element; return configProperties.problemMatcher === undefined && !task.hasDefinedMatchers; } return false; } - private async updateNeverProblemMatcherSetting(type: string): Promise<void> { - const current = this.configurationService.getValue(PROBLEM_MATCHER_NEVER_CONFIG); + private async _updateNeverProblemMatcherSetting(type: string): Promise<void> { + const current = this._configurationService.getValue(PROBLEM_MATCHER_NEVER_CONFIG); if (current === true) { return; } @@ -1154,19 +1156,19 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer newValue = Object.create(null); } newValue[type] = true; - return this.configurationService.updateValue(PROBLEM_MATCHER_NEVER_CONFIG, newValue); + return this._configurationService.updateValue(PROBLEM_MATCHER_NEVER_CONFIG, newValue); } - private attachProblemMatcher(task: ContributedTask | CustomTask): Promise<Task | undefined> { - interface ProblemMatcherPickEntry extends IQuickPickItem { - matcher: NamedProblemMatcher | undefined; + private _attachProblemMatcher(task: ContributedTask | CustomTask): Promise<Task | undefined> { + interface IProblemMatcherPickEntry extends IQuickPickItem { + matcher: INamedProblemMatcher | undefined; never?: boolean; learnMore?: boolean; setting?: string; } - let entries: QuickPickInput<ProblemMatcherPickEntry>[] = []; - for (let key of ProblemMatcherRegistry.keys()) { - let matcher = ProblemMatcherRegistry.get(key); + let entries: QuickPickInput<IProblemMatcherPickEntry>[] = []; + for (const key of ProblemMatcherRegistry.keys()) { + const matcher = ProblemMatcherRegistry.get(key); if (matcher.deprecated) { continue; } @@ -1191,7 +1193,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer entries.unshift({ type: 'separator', label: nls.localize('TaskService.associate', 'associate') }); let taskType: string; if (CustomTask.is(task)) { - let configProperties: TaskConfig.ConfigurationProperties = task._source.config.element; + const configProperties: TaskConfig.IConfigurationProperties = task._source.config.element; taskType = (<any>configProperties).type; } else { taskType = task.getDefinition().type; @@ -1202,22 +1204,22 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer { label: nls.localize('TaskService.attachProblemMatcher.neverType', 'Never scan the task output for {0} tasks', taskType), matcher: undefined, setting: taskType }, { label: nls.localize('TaskService.attachProblemMatcher.learnMoreAbout', 'Learn more about scanning the task output'), matcher: undefined, learnMore: true } ); - return this.quickInputService.pick(entries, { + return this._quickInputService.pick(entries, { placeHolder: nls.localize('selectProblemMatcher', 'Select for which kind of errors and warnings to scan the task output'), }).then(async (selected) => { if (selected) { if (selected.learnMore) { - this.openDocumentation(); + this._openDocumentation(); return undefined; } else if (selected.never) { this.customize(task, { problemMatcher: [] }, true); return task; } else if (selected.matcher) { - let newTask = task.clone(); - let matcherReference = `$${selected.matcher.name}`; - let properties: CustomizationProperties = { problemMatcher: [matcherReference] }; + const newTask = task.clone(); + const matcherReference = `$${selected.matcher.name}`; + const properties: ICustomizationProperties = { problemMatcher: [matcherReference] }; newTask.configurationProperties.problemMatchers = [matcherReference]; - let matcher = ProblemMatcherRegistry.get(selected.matcher.name); + const matcher = ProblemMatcherRegistry.get(selected.matcher.name); if (matcher && matcher.watching !== undefined) { properties.isBackground = true; newTask.configurationProperties.isBackground = true; @@ -1225,7 +1227,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.customize(task, properties, true); return newTask; } else if (selected.setting) { - await this.updateNeverProblemMatcherSetting(selected.setting); + await this._updateNeverProblemMatcherSetting(selected.setting); return task; } else { return task; @@ -1238,12 +1240,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return Promise.resolve(task); } - private getTasksForGroup(group: TaskGroup): Promise<Task[]> { - return this.getGroupedTasks().then((groups) => { - let result: Task[] = []; + private _getTasksForGroup(group: TaskGroup): Promise<Task[]> { + return this._getGroupedTasks().then((groups) => { + const result: Task[] = []; groups.forEach((tasks) => { - for (let task of tasks) { - let configTaskGroup = TaskGroup.from(task.configurationProperties.group); + for (const task of tasks) { + const configTaskGroup = TaskGroup.from(task.configurationProperties.group); if (configTaskGroup?._id === group._id) { result.push(task); } @@ -1254,10 +1256,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } public needsFolderQualification(): boolean { - return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; + return this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; } - private canCustomize(task: Task): boolean { + private _canCustomize(task: Task): boolean { if (this.schemaVersion !== JsonSchemaVersion.V2_0_0) { return false; } @@ -1270,11 +1272,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return false; } - private async formatTaskForJson(resource: URI, task: TaskConfig.CustomTask | TaskConfig.ConfiguringTask): Promise<string> { + private async _formatTaskForJson(resource: URI, task: TaskConfig.ICustomTask | TaskConfig.IConfiguringTask): Promise<string> { let reference: IReference<IResolvedTextEditorModel> | undefined; let stringValue: string = ''; try { - reference = await this.textModelResolverService.createModelReference(resource); + reference = await this._textModelResolverService.createModelReference(resource); const model = reference.object.textEditorModel; const { tabSize, insertSpaces } = model.getOptions(); const eol = model.getEOL(); @@ -1291,12 +1293,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return stringValue; } - private openEditorAtTask(resource: URI | undefined, task: TaskConfig.CustomTask | TaskConfig.ConfiguringTask | string | undefined, configIndex: number = -1): Promise<boolean> { + private _openEditorAtTask(resource: URI | undefined, task: TaskConfig.ICustomTask | TaskConfig.IConfiguringTask | string | undefined, configIndex: number = -1): Promise<boolean> { if (resource === undefined) { return Promise.resolve(false); } let selection: ITextEditorSelection | undefined; - return this.fileService.readFile(resource).then(content => content.value).then(async content => { + return this._fileService.readFile(resource).then(content => content.value).then(async content => { if (!content) { return false; } @@ -1304,16 +1306,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const contentValue = content.toString(); let stringValue: string | undefined; if (configIndex !== -1) { - const json: TaskConfig.ExternalTaskRunnerConfiguration = this.configurationService.getValue<TaskConfig.ExternalTaskRunnerConfiguration>('tasks', { resource }); + const json: TaskConfig.IExternalTaskRunnerConfiguration = this._configurationService.getValue<TaskConfig.IExternalTaskRunnerConfiguration>('tasks', { resource }); if (json.tasks && (json.tasks.length > configIndex)) { - stringValue = await this.formatTaskForJson(resource, json.tasks[configIndex]); + stringValue = await this._formatTaskForJson(resource, json.tasks[configIndex]); } } if (!stringValue) { if (typeof task === 'string') { stringValue = task; } else { - stringValue = await this.formatTaskForJson(resource, task); + stringValue = await this._formatTaskForJson(resource, task); } } @@ -1333,7 +1335,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer selection = startLineNumber > 1 ? { startLineNumber, startColumn: startLineNumber === endLineNumber ? 4 : 3, endLineNumber, endColumn: startLineNumber === endLineNumber ? undefined : 4 } : undefined; } - return this.editorService.openEditor({ + return this._editorService.openEditor({ resource, options: { pinned: false, @@ -1345,15 +1347,15 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - private createCustomizableTask(task: ContributedTask | CustomTask | ConfiguringTask): TaskConfig.CustomTask | TaskConfig.ConfiguringTask | undefined { - let toCustomize: TaskConfig.CustomTask | TaskConfig.ConfiguringTask | undefined; - let taskConfig = CustomTask.is(task) || ConfiguringTask.is(task) ? task._source.config : undefined; + private _createCustomizableTask(task: ContributedTask | CustomTask | ConfiguringTask): TaskConfig.ICustomTask | TaskConfig.IConfiguringTask | undefined { + let toCustomize: TaskConfig.ICustomTask | TaskConfig.IConfiguringTask | undefined; + const taskConfig = CustomTask.is(task) || ConfiguringTask.is(task) ? task._source.config : undefined; if (taskConfig && taskConfig.element) { toCustomize = { ...(taskConfig.element) }; } else if (ContributedTask.is(task)) { toCustomize = { }; - let identifier: TaskConfig.TaskIdentifier = Object.assign(Object.create(null), task.defines); + const identifier: TaskConfig.ITaskIdentifier = Object.assign(Object.create(null), task.defines); delete identifier['_key']; Object.keys(identifier).forEach(key => (<any>toCustomize)![key] = identifier[key]); if (task.configurationProperties.problemMatchers && task.configurationProperties.problemMatchers.length > 0 && Types.isStringArray(task.configurationProperties.problemMatchers)) { @@ -1378,8 +1380,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return toCustomize; } - public async customize(task: ContributedTask | CustomTask | ConfiguringTask, properties?: CustomizationProperties, openConfig?: boolean): Promise<void> { - if (!(await this.trust())) { + public async customize(task: ContributedTask | CustomTask | ConfiguringTask, properties?: ICustomizationProperties, openConfig?: boolean): Promise<void> { + if (!(await this._trust())) { return; } @@ -1387,21 +1389,21 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!workspaceFolder) { return Promise.resolve(undefined); } - let configuration = this.getConfiguration(workspaceFolder, task._source.kind); + const configuration = this._getConfiguration(workspaceFolder, task._source.kind); if (configuration.hasParseErrors) { - this.notificationService.warn(nls.localize('customizeParseErrors', 'The current task configuration has errors. Please fix the errors first before customizing a task.')); + this._notificationService.warn(nls.localize('customizeParseErrors', 'The current task configuration has errors. Please fix the errors first before customizing a task.')); return Promise.resolve<void>(undefined); } - let fileConfig = configuration.config; - const toCustomize = this.createCustomizableTask(task); + const fileConfig = configuration.config; + const toCustomize = this._createCustomizableTask(task); if (!toCustomize) { return Promise.resolve(undefined); } const index: number | undefined = CustomTask.is(task) ? task._source.config.index : undefined; if (properties) { - for (let property of Object.getOwnPropertyNames(properties)) { - let value = (<any>properties)[property]; + for (const property of Object.getOwnPropertyNames(properties)) { + const value = (<any>properties)[property]; if (value !== undefined && value !== null) { (<any>toCustomize)[property] = value; } @@ -1410,7 +1412,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer let promise: Promise<void> | undefined; if (!fileConfig) { - let value = { + const value = { version: '2.0.0', tasks: [toCustomize] }; @@ -1418,20 +1420,20 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer '{', nls.localize('tasksJsonComment', '\t// See https://go.microsoft.com/fwlink/?LinkId=733558 \n\t// for the documentation about the tasks.json format'), ].join('\n') + JSON.stringify(value, null, '\t').substr(1); - let editorConfig = this.configurationService.getValue<any>(); + const editorConfig = this._configurationService.getValue<any>(); if (editorConfig.editor.insertSpaces) { content = content.replace(/(\n)(\t+)/g, (_, s1, s2) => s1 + ' '.repeat(s2.length * editorConfig.editor.tabSize)); } - promise = this.textFileService.create([{ resource: workspaceFolder.toResource('.vscode/tasks.json'), value: content }]).then(() => { }); + promise = this._textFileService.create([{ resource: workspaceFolder.toResource('.vscode/tasks.json'), value: content }]).then(() => { }); } else { // We have a global task configuration if ((index === -1) && properties) { if (properties.problemMatcher !== undefined) { fileConfig.problemMatcher = properties.problemMatcher; - promise = this.writeConfiguration(workspaceFolder, 'tasks.problemMatchers', fileConfig.problemMatcher, task._source.kind); + promise = this._writeConfiguration(workspaceFolder, 'tasks.problemMatchers', fileConfig.problemMatcher, task._source.kind); } else if (properties.group !== undefined) { fileConfig.group = properties.group; - promise = this.writeConfiguration(workspaceFolder, 'tasks.group', fileConfig.group, task._source.kind); + promise = this._writeConfiguration(workspaceFolder, 'tasks.group', fileConfig.group, task._source.kind); } } else { if (!Array.isArray(fileConfig.tasks)) { @@ -1442,7 +1444,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } else { fileConfig.tasks[index] = toCustomize; } - promise = this.writeConfiguration(workspaceFolder, 'tasks.tasks', fileConfig.tasks, task._source.kind); + promise = this._writeConfiguration(workspaceFolder, 'tasks.tasks', fileConfig.tasks, task._source.kind); } } if (!promise) { @@ -1450,34 +1452,34 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } return promise.then(() => { if (openConfig) { - this.openEditorAtTask(this.getResourceForTask(task), toCustomize); + this._openEditorAtTask(this._getResourceForTask(task), toCustomize); } }); } - private writeConfiguration(workspaceFolder: IWorkspaceFolder, key: string, value: any, source?: string): Promise<void> | undefined { + private _writeConfiguration(workspaceFolder: IWorkspaceFolder, key: string, value: any, source?: string): Promise<void> | undefined { let target: ConfigurationTarget | undefined = undefined; switch (source) { case TaskSourceKind.User: target = ConfigurationTarget.USER; break; case TaskSourceKind.WorkspaceFile: target = ConfigurationTarget.WORKSPACE; break; - default: if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + default: if (this._contextService.getWorkbenchState() === WorkbenchState.FOLDER) { target = ConfigurationTarget.WORKSPACE; - } else if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + } else if (this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { target = ConfigurationTarget.WORKSPACE_FOLDER; } } if (target) { - return this.configurationService.updateValue(key, value, { resource: workspaceFolder.uri }, target); + return this._configurationService.updateValue(key, value, { resource: workspaceFolder.uri }, target); } else { return undefined; } } - private getResourceForKind(kind: string): URI | undefined { - this.updateSetup(); + private _getResourceForKind(kind: string): URI | undefined { + this._updateSetup(); switch (kind) { case TaskSourceKind.User: { - return resources.joinPath(resources.dirname(this.preferencesService.userSettingsResource), 'tasks.json'); + return resources.joinPath(resources.dirname(this._preferencesService.userSettingsResource), 'tasks.json'); } case TaskSourceKind.WorkspaceFile: { if (this._workspace && this._workspace.configuration) { @@ -1490,9 +1492,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private getResourceForTask(task: CustomTask | ConfiguringTask | ContributedTask): URI { + private _getResourceForTask(task: CustomTask | ConfiguringTask | ContributedTask): URI { if (CustomTask.is(task)) { - let uri = this.getResourceForKind(task._source.kind); + let uri = this._getResourceForKind(task._source.kind); if (!uri) { const taskFolder = task.getWorkspaceFolder(); if (taskFolder) { @@ -1510,23 +1512,23 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer public async openConfig(task: CustomTask | ConfiguringTask | undefined): Promise<boolean> { let resource: URI | undefined; if (task) { - resource = this.getResourceForTask(task); + resource = this._getResourceForTask(task); } else { resource = (this._workspaceFolders && (this._workspaceFolders.length > 0)) ? this._workspaceFolders[0].toResource('.vscode/tasks.json') : undefined; } - return this.openEditorAtTask(resource, task ? task._label : undefined, task ? task._source.config.index : -1); + return this._openEditorAtTask(resource, task ? task._label : undefined, task ? task._source.config.index : -1); } - private createRunnableTask(tasks: TaskMap, group: TaskGroup): { task: Task; resolver: ITaskResolver } | undefined { - interface ResolverData { + private _createRunnableTask(tasks: TaskMap, group: TaskGroup): { task: Task; resolver: ITaskResolver } | undefined { + interface IResolverData { id: Map<string, Task>; label: Map<string, Task>; identifier: Map<string, Task>; } - let resolverData: Map<string, ResolverData> = new Map(); - let workspaceTasks: Task[] = []; - let extensionTasks: Task[] = []; + const resolverData: Map<string, IResolverData> = new Map(); + const workspaceTasks: Task[] = []; + const extensionTasks: Task[] = []; tasks.forEach((tasks, folder) => { let data = resolverData.get(folder); if (!data) { @@ -1537,7 +1539,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; resolverData.set(folder, data); } - for (let task of tasks) { + for (const task of tasks) { data.id.set(task._id, task); data.label.set(task._label, task); if (task.configurationProperties.identifier) { @@ -1552,9 +1554,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } }); - let resolver: ITaskResolver = { + const resolver: ITaskResolver = { resolve: async (uri: URI | string, alias: string) => { - let data = resolverData.get(typeof uri === 'string' ? uri : uri.toString()); + const data = resolverData.get(typeof uri === 'string' ? uri : uri.toString()); if (!data) { return undefined; } @@ -1576,8 +1578,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (extensionTasks.length === 1) { return { task: extensionTasks[0], resolver }; } else { - let id: string = UUID.generateUuid(); - let task: InMemoryTask = new InMemoryTask( + const id: string = UUID.generateUuid(); + const task: InMemoryTask = new InMemoryTask( id, { kind: TaskSourceKind.InMemory, label: 'inMemory' }, id, @@ -1593,7 +1595,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private createResolver(grouped?: TaskMap): ITaskResolver { + private _createResolver(grouped?: TaskMap): ITaskResolver { interface ResolverData { label: Map<string, Task>; identifier: Map<string, Task>; @@ -1602,7 +1604,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer let resolverData: Map<string, ResolverData> | undefined; - async function quickResolve(that: AbstractTaskService, uri: URI | string, identifier: string | TaskIdentifier) { + async function quickResolve(that: AbstractTaskService, uri: URI | string, identifier: string | ITaskIdentifier) { const foundTasks = await that._findWorkspaceTasks((task: Task | ConfiguringTask): boolean => { const taskUri = ((ConfiguringTask.is(task) || CustomTask.is(task)) ? task._source.config.workspaceFolder?.uri : undefined); const originalUri = (typeof uri === 'string' ? uri : uri.toString()); @@ -1630,18 +1632,18 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer async function getResolverData(that: AbstractTaskService) { if (resolverData === undefined) { resolverData = new Map(); - (grouped || await that.getGroupedTasks()).forEach((tasks, folder) => { + (grouped || await that._getGroupedTasks()).forEach((tasks, folder) => { let data = resolverData!.get(folder); if (!data) { data = { label: new Map<string, Task>(), identifier: new Map<string, Task>(), taskIdentifier: new Map<string, Task>() }; resolverData!.set(folder, data); } - for (let task of tasks) { + for (const task of tasks) { data.label.set(task._label, task); if (task.configurationProperties.identifier) { data.identifier.set(task.configurationProperties.identifier, task); } - let keyedIdentifier = task.getDefinition(true); + const keyedIdentifier = task.getDefinition(true); if (keyedIdentifier !== undefined) { data.taskIdentifier.set(keyedIdentifier._key, task); } @@ -1651,22 +1653,22 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return resolverData; } - async function fullResolve(that: AbstractTaskService, uri: URI | string, identifier: string | TaskIdentifier) { + async function fullResolve(that: AbstractTaskService, uri: URI | string, identifier: string | ITaskIdentifier) { const allResolverData = await getResolverData(that); - let data = allResolverData.get(typeof uri === 'string' ? uri : uri.toString()); + const data = allResolverData.get(typeof uri === 'string' ? uri : uri.toString()); if (!data) { return undefined; } if (Types.isString(identifier)) { return data.label.get(identifier) || data.identifier.get(identifier); } else { - let key = TaskDefinition.createTaskIdentifier(identifier, console); + const key = TaskDefinition.createTaskIdentifier(identifier, console); return key !== undefined ? data.taskIdentifier.get(key._key) : undefined; } } return { - resolve: async (uri: URI | string, identifier: string | TaskIdentifier | undefined) => { + resolve: async (uri: URI | string, identifier: string | ITaskIdentifier | undefined) => { if (!identifier) { return undefined; } @@ -1679,19 +1681,19 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; } - private async saveBeforeRun(): Promise<boolean> { + private async _saveBeforeRun(): Promise<boolean> { enum SaveBeforeRunConfigOptions { Always = 'always', Never = 'never', Prompt = 'prompt' } - const saveBeforeRunTaskConfig: SaveBeforeRunConfigOptions = this.configurationService.getValue('task.saveBeforeRun'); + const saveBeforeRunTaskConfig: SaveBeforeRunConfigOptions = this._configurationService.getValue('task.saveBeforeRun'); if (saveBeforeRunTaskConfig === SaveBeforeRunConfigOptions.Never) { return false; } else if (saveBeforeRunTaskConfig === SaveBeforeRunConfigOptions.Prompt) { - const dialogOptions = await this.dialogService.show( + const dialogOptions = await this._dialogService.show( Severity.Info, nls.localize('TaskSystem.saveBeforeRun.prompt.title', 'Save all editors?'), [nls.localize('saveBeforeRun.save', 'Save'), nls.localize('saveBeforeRun.dontSave', 'Don\'t save')], @@ -1705,15 +1707,15 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return false; } } - await this.editorService.saveAll({ reason: SaveReason.AUTO }); + await this._editorService.saveAll({ reason: SaveReason.AUTO }); return true; } - private async executeTask(task: Task, resolver: ITaskResolver, runSource: TaskRunSource): Promise<ITaskSummary> { + private async _executeTask(task: Task, resolver: ITaskResolver, runSource: TaskRunSource): Promise<ITaskSummary> { let taskToRun: Task = task; - if (await this.saveBeforeRun()) { - await this.configurationService.reloadConfiguration(); - await this.updateWorkspaceTasks(); + if (await this._saveBeforeRun()) { + await this._configurationService.reloadConfiguration(); + await this._updateWorkspaceTasks(); const taskFolder = task.getWorkspaceFolder(); const taskIdentifier = task.configurationProperties.identifier; // Since we save before running tasks, the task may have changed as part of the save. @@ -1723,28 +1725,28 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer ? await this.getTask(taskFolder, taskIdentifier) : task) ?? task; } await ProblemMatcherRegistry.onReady(); - let executeResult = this.getTaskSystem().run(taskToRun, resolver); - return this.handleExecuteResult(executeResult, runSource); + const executeResult = this._getTaskSystem().run(taskToRun, resolver); + return this._handleExecuteResult(executeResult, runSource); } - private async handleExecuteResult(executeResult: ITaskExecuteResult, runSource?: TaskRunSource): Promise<ITaskSummary> { + private async _handleExecuteResult(executeResult: ITaskExecuteResult, runSource?: TaskRunSource): Promise<ITaskSummary> { if (runSource === TaskRunSource.User) { - await this.setRecentlyUsedTask(executeResult.task); + await this._setRecentlyUsedTask(executeResult.task); } if (executeResult.kind === TaskExecuteKind.Active) { - let active = executeResult.active; + const active = executeResult.active; if (active && active.same) { if (this._taskSystem?.isTaskVisible(executeResult.task)) { const message = nls.localize('TaskSystem.activeSame.noBackground', 'The task \'{0}\' is already active.', executeResult.task.getQualifiedLabel()); - let lastInstance = this.getTaskSystem().getLastInstance(executeResult.task) ?? executeResult.task; - this.notificationService.prompt(Severity.Warning, message, + const lastInstance = this._getTaskSystem().getLastInstance(executeResult.task) ?? executeResult.task; + this._notificationService.prompt(Severity.Warning, message, [{ label: nls.localize('terminateTask', "Terminate Task"), run: () => this.terminate(lastInstance) }, { label: nls.localize('restartTask', "Restart Task"), - run: () => this.restart(lastInstance) + run: () => this._restart(lastInstance) }], { sticky: true } ); @@ -1758,7 +1760,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return executeResult.promise; } - private restart(task: Task): void { + private _restart(task: Task): void { if (!this._taskSystem) { return; } @@ -1768,14 +1770,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer // eat the error, it has already been surfaced to the user and we don't care about it here }); } else { - this.notificationService.warn(nls.localize('TaskSystem.restartFailed', 'Failed to terminate and restart task {0}', Types.isString(task) ? task : task.configurationProperties.name)); + this._notificationService.warn(nls.localize('TaskSystem.restartFailed', 'Failed to terminate and restart task {0}', Types.isString(task) ? task : task.configurationProperties.name)); } return response; }); } - public async terminate(task: Task): Promise<TaskTerminateResponse> { - if (!(await this.trust())) { + public async terminate(task: Task): Promise<ITaskTerminateResponse> { + if (!(await this._trust())) { return { success: true, task: undefined }; } @@ -1785,24 +1787,24 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return this._taskSystem.terminate(task); } - private terminateAll(): Promise<TaskTerminateResponse[]> { + private _terminateAll(): Promise<ITaskTerminateResponse[]> { if (!this._taskSystem) { - return Promise.resolve<TaskTerminateResponse[]>([]); + return Promise.resolve<ITaskTerminateResponse[]>([]); } return this._taskSystem.terminateAll(); } - protected createTerminalTaskSystem(): ITaskSystem { + protected _createTerminalTaskSystem(): ITaskSystem { return new TerminalTaskSystem( - this.terminalService, this.terminalGroupService, this.outputService, this.paneCompositeService, this.viewsService, this.markerService, - this.modelService, this.configurationResolverService, - this.contextService, this.environmentService, - AbstractTaskService.OutputChannelId, this.fileService, this.terminalProfileResolverService, - this.pathService, this.viewDescriptorService, this.logService, this.configurationService, this.notificationService, + this._terminalService, this._terminalGroupService, this._outputService, this._paneCompositeService, this._viewsService, this._markerService, + this._modelService, this._configurationResolverService, + this._contextService, this._environmentService, + AbstractTaskService.OutputChannelId, this._fileService, this._terminalProfileResolverService, + this._pathService, this._viewDescriptorService, this._logService, this._configurationService, this._notificationService, this, (workspaceFolder: IWorkspaceFolder | undefined) => { if (workspaceFolder) { - return this.getTaskSystemInfo(workspaceFolder.uri.scheme); + return this._getTaskSystemInfo(workspaceFolder.uri.scheme); } else if (this._taskSystemInfos.size > 0) { const infos = Array.from(this._taskSystemInfos.entries()); const notFile = infos.filter(info => info[0] !== Schemas.file); @@ -1817,24 +1819,24 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer ); } - protected abstract getTaskSystem(): ITaskSystem; + protected abstract _getTaskSystem(): ITaskSystem; - private isTaskProviderEnabled(type: string) { + private _isTaskProviderEnabled(type: string) { const definition = TaskDefinitionRegistry.get(type); - return !definition || !definition.when || this.contextKeyService.contextMatchesRules(definition.when); + return !definition || !definition.when || this._contextKeyService.contextMatchesRules(definition.when); } - private getGroupedTasks(type?: string): Promise<TaskMap> { - const needsRecentTasksMigration = this.needsRecentTasksMigration(); + private _getGroupedTasks(type?: string): Promise<TaskMap> { + const needsRecentTasksMigration = this._needsRecentTasksMigration(); return this._activateTaskProviders(type).then(() => { - let validTypes: IStringDictionary<boolean> = Object.create(null); + const validTypes: IStringDictionary<boolean> = Object.create(null); TaskDefinitionRegistry.all().forEach(definition => validTypes[definition.taskType] = true); validTypes['shell'] = true; validTypes['process'] = true; - return new Promise<TaskSet[]>(resolve => { - let result: TaskSet[] = []; + return new Promise<ITaskSet[]>(resolve => { + const result: ITaskSet[] = []; let counter: number = 0; - let done = (value: TaskSet | undefined) => { + const done = (value: ITaskSet | undefined) => { if (value) { result.push(value); } @@ -1842,16 +1844,16 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer resolve(result); } }; - let error = (error: any) => { + const error = (error: any) => { try { if (error && Types.isString(error.message)) { this._outputChannel.append('Error: '); this._outputChannel.append(error.message); this._outputChannel.append('\n'); - this.showOutput(); + this._showOutput(); } else { this._outputChannel.append('Unknown error received while collecting tasks from providers.\n'); - this.showOutput(); + this._showOutput(); } } finally { if (--counter === 0) { @@ -1859,23 +1861,23 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } }; - if (this.isProvideTasksEnabled() && (this.schemaVersion === JsonSchemaVersion.V2_0_0) && (this._providers.size > 0)) { + if (this._isProvideTasksEnabled() && (this.schemaVersion === JsonSchemaVersion.V2_0_0) && (this._providers.size > 0)) { let foundAnyProviders = false; for (const [handle, provider] of this._providers) { const providerType = this._providerTypes.get(handle); if ((type === undefined) || (type === providerType)) { - if (providerType && !this.isTaskProviderEnabled(providerType)) { + if (providerType && !this._isTaskProviderEnabled(providerType)) { continue; } foundAnyProviders = true; counter++; - provider.provideTasks(validTypes).then((taskSet: TaskSet) => { + provider.provideTasks(validTypes).then((taskSet: ITaskSet) => { // Check that the tasks provided are of the correct type for (const task of taskSet.tasks) { if (task.type !== this._providerTypes.get(handle)) { this._outputChannel.append(nls.localize('unexpectedTaskType', "The task provider for \"{0}\" tasks unexpectedly provided a task of type \"{1}\".\n", this._providerTypes.get(handle), task.type)); if ((task.type !== 'shell') && (task.type !== 'process')) { - this.showOutput(); + this._showOutput(); } break; } @@ -1892,12 +1894,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } }); }).then((contributedTaskSets) => { - let result: TaskMap = new TaskMap(); - let contributedTasks: TaskMap = new TaskMap(); + const result: TaskMap = new TaskMap(); + const contributedTasks: TaskMap = new TaskMap(); - for (let set of contributedTaskSets) { - for (let task of set.tasks) { - let workspaceFolder = task.getWorkspaceFolder(); + for (const set of contributedTaskSets) { + for (const task of set.tasks) { + const workspaceFolder = task.getWorkspaceFolder(); if (workspaceFolder) { contributedTasks.add(workspaceFolder, task); } @@ -1907,7 +1909,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return this.getWorkspaceTasks().then(async (customTasks) => { const customTasksKeyValuePairs = Array.from(customTasks); const customTasksPromises = customTasksKeyValuePairs.map(async ([key, folderTasks]) => { - let contributed = contributedTasks.get(key); + const contributed = contributedTasks.get(key); if (!folderTasks.set) { if (contributed) { result.add(key, ...contributed); @@ -1915,23 +1917,23 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return; } - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + if (this._contextService.getWorkbenchState() === WorkbenchState.EMPTY) { result.add(key, ...folderTasks.set.tasks); } else { - let configurations = folderTasks.configurations; - let legacyTaskConfigurations = folderTasks.set ? this.getLegacyTaskConfigurations(folderTasks.set) : undefined; - let customTasksToDelete: Task[] = []; + const configurations = folderTasks.configurations; + const legacyTaskConfigurations = folderTasks.set ? this._getLegacyTaskConfigurations(folderTasks.set) : undefined; + const customTasksToDelete: Task[] = []; if (configurations || legacyTaskConfigurations) { - let unUsedConfigurations: Set<string> = new Set<string>(); + const unUsedConfigurations: Set<string> = new Set<string>(); if (configurations) { Object.keys(configurations.byIdentifier).forEach(key => unUsedConfigurations.add(key)); } - for (let task of contributed) { + for (const task of contributed) { if (!ContributedTask.is(task)) { continue; } if (configurations) { - let configuringTask = configurations.byIdentifier[task.defines._key]; + const configuringTask = configurations.byIdentifier[task.defines._key]; if (configuringTask) { unUsedConfigurations.delete(task.defines._key); result.add(key, TaskConfig.createCustomTask(task, configuringTask)); @@ -1939,7 +1941,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer result.add(key, task); } } else if (legacyTaskConfigurations) { - let configuringTask = legacyTaskConfigurations[task.defines._key]; + const configuringTask = legacyTaskConfigurations[task.defines._key]; if (configuringTask) { result.add(key, TaskConfig.createCustomTask(task, configuringTask)); customTasksToDelete.push(configuringTask); @@ -1951,11 +1953,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } if (customTasksToDelete.length > 0) { - let toDelete = customTasksToDelete.reduce<IStringDictionary<boolean>>((map, task) => { + const toDelete = customTasksToDelete.reduce<IStringDictionary<boolean>>((map, task) => { map[task._id] = true; return map; }, Object.create(null)); - for (let task of folderTasks.set.tasks) { + for (const task of folderTasks.set.tasks) { if (toDelete[task._id]) { continue; } @@ -1968,7 +1970,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const unUsedConfigurationsAsArray = Array.from(unUsedConfigurations); const unUsedConfigurationPromises = unUsedConfigurationsAsArray.map(async (value) => { - let configuringTask = configurations!.byIdentifier[value]; + const configuringTask = configurations!.byIdentifier[value]; if (type && (type !== configuringTask.configures.type)) { return; } @@ -1978,7 +1980,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer for (const [handle, provider] of this._providers) { const providerType = this._providerTypes.get(handle); if (configuringTask.type === providerType) { - if (providerType && !this.isTaskProviderEnabled(providerType)) { + if (providerType && !this._isTaskProviderEnabled(providerType)) { requiredTaskProviderUnavailable = true; continue; } @@ -2008,7 +2010,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer configuringTask.configures.type, JSON.stringify(configuringTask._source.config.element, undefined, 4) )); - this.showOutput(); + this._showOutput(); } }); @@ -2023,14 +2025,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer await Promise.all(customTasksPromises); if (needsRecentTasksMigration) { // At this point we have all the tasks and can migrate the recently used tasks. - await this.migrateRecentTasks(result.all()); + await this._migrateRecentTasks(result.all()); } return result; }, () => { // If we can't read the tasks.json file provide at least the contributed tasks - let result: TaskMap = new TaskMap(); - for (let set of contributedTaskSets) { - for (let task of set.tasks) { + const result: TaskMap = new TaskMap(); + for (const set of contributedTaskSets) { + for (const task of set.tasks) { const folder = task.getWorkspaceFolder(); if (folder) { result.add(folder, task); @@ -2042,7 +2044,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - private getLegacyTaskConfigurations(workspaceTasks: TaskSet): IStringDictionary<CustomTask> | undefined { + private _getLegacyTaskConfigurations(workspaceTasks: ITaskSet): IStringDictionary<CustomTask> | undefined { let result: IStringDictionary<CustomTask> | undefined; function getResult(): IStringDictionary<CustomTask> { if (result) { @@ -2051,13 +2053,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer result = Object.create(null); return result!; } - for (let task of workspaceTasks.tasks) { + for (const task of workspaceTasks.tasks) { if (CustomTask.is(task)) { - let commandName = task.command && task.command.name; + const commandName = task.command && task.command.name; // This is for backwards compatibility with the 0.1.0 task annotation code // if we had a gulp, jake or grunt command a task specification was a annotation if (commandName === 'gulp' || commandName === 'grunt' || commandName === 'jake') { - let identifier = NKeyedTaskIdentifier.create({ + const identifier = KeyedTaskIdentifier.create({ type: commandName, task: task.configurationProperties.name }); @@ -2068,53 +2070,53 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return result; } - public async getWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise<Map<string, WorkspaceFolderTaskResult>> { - if (!(await this.trust())) { + public async getWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise<Map<string, IWorkspaceFolderTaskResult>> { + if (!(await this._trust())) { return new Map(); } await this._waitForSupportedExecutions; if (this._workspaceTasksPromise) { return this._workspaceTasksPromise; } - return this.updateWorkspaceTasks(runSource); + return this._updateWorkspaceTasks(runSource); } - private updateWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise<Map<string, WorkspaceFolderTaskResult>> { - this._workspaceTasksPromise = this.computeWorkspaceTasks(runSource); + private _updateWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise<Map<string, IWorkspaceFolderTaskResult>> { + this._workspaceTasksPromise = this._computeWorkspaceTasks(runSource); return this._workspaceTasksPromise; } - private async getAFolder(): Promise<IWorkspaceFolder> { + private async _getAFolder(): Promise<IWorkspaceFolder> { let folder = this.workspaceFolders.length > 0 ? this.workspaceFolders[0] : undefined; if (!folder) { - const userhome = await this.pathService.userHome(); + const userhome = await this._pathService.userHome(); folder = new WorkspaceFolder({ uri: userhome, name: resources.basename(userhome), index: 0 }); } return folder; } - protected computeWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise<Map<string, WorkspaceFolderTaskResult>> { - let promises: Promise<WorkspaceFolderTaskResult | undefined>[] = []; - for (let folder of this.workspaceFolders) { - promises.push(this.computeWorkspaceFolderTasks(folder, runSource).then((value) => value, () => undefined)); + protected _computeWorkspaceTasks(runSource: TaskRunSource = TaskRunSource.User): Promise<Map<string, IWorkspaceFolderTaskResult>> { + const promises: Promise<IWorkspaceFolderTaskResult | undefined>[] = []; + for (const folder of this.workspaceFolders) { + promises.push(this._computeWorkspaceFolderTasks(folder, runSource).then((value) => value, () => undefined)); } return Promise.all(promises).then(async (values) => { - let result = new Map<string, WorkspaceFolderTaskResult>(); - for (let value of values) { + const result = new Map<string, IWorkspaceFolderTaskResult>(); + for (const value of values) { if (value) { result.set(value.workspaceFolder.uri.toString(), value); } } - const folder = await this.getAFolder(); - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY) { - const workspaceFileTasks = await this.computeWorkspaceFileTasks(folder, runSource).then((value) => value, () => undefined); + const folder = await this._getAFolder(); + if (this._contextService.getWorkbenchState() !== WorkbenchState.EMPTY) { + const workspaceFileTasks = await this._computeWorkspaceFileTasks(folder, runSource).then((value) => value, () => undefined); if (workspaceFileTasks && this._workspace && this._workspace.configuration) { result.set(this._workspace.configuration.toString(), workspaceFileTasks); } } - const userTasks = await this.computeUserTasks(folder, runSource).then((value) => value, () => undefined); + const userTasks = await this._computeUserTasks(folder, runSource).then((value) => value, () => undefined); if (userTasks) { result.set(USER_TASKS_GROUP_KEY, userTasks); } @@ -2122,26 +2124,26 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - private get jsonTasksSupported(): boolean { - return !!ShellExecutionSupportedContext.getValue(this.contextKeyService) && !!ProcessExecutionSupportedContext.getValue(this.contextKeyService); + private get _jsonTasksSupported(): boolean { + return !!ShellExecutionSupportedContext.getValue(this._contextKeyService) && !!ProcessExecutionSupportedContext.getValue(this._contextKeyService); } - private computeWorkspaceFolderTasks(workspaceFolder: IWorkspaceFolder, runSource: TaskRunSource = TaskRunSource.User): Promise<WorkspaceFolderTaskResult> { - return (this.executionEngine === ExecutionEngine.Process - ? this.computeLegacyConfiguration(workspaceFolder) - : this.computeConfiguration(workspaceFolder)). + private _computeWorkspaceFolderTasks(workspaceFolder: IWorkspaceFolder, runSource: TaskRunSource = TaskRunSource.User): Promise<IWorkspaceFolderTaskResult> { + return (this._executionEngine === ExecutionEngine.Process + ? this._computeLegacyConfiguration(workspaceFolder) + : this._computeConfiguration(workspaceFolder)). then((workspaceFolderConfiguration) => { if (!workspaceFolderConfiguration || !workspaceFolderConfiguration.config || workspaceFolderConfiguration.hasErrors) { return Promise.resolve({ workspaceFolder, set: undefined, configurations: undefined, hasErrors: workspaceFolderConfiguration ? workspaceFolderConfiguration.hasErrors : false }); } - return ProblemMatcherRegistry.onReady().then(async (): Promise<WorkspaceFolderTaskResult> => { - let taskSystemInfo: TaskSystemInfo | undefined = this.getTaskSystemInfo(workspaceFolder.uri.scheme); - let problemReporter = new ProblemReporter(this._outputChannel); - let parseResult = TaskConfig.parse(workspaceFolder, undefined, taskSystemInfo ? taskSystemInfo.platform : Platform.platform, workspaceFolderConfiguration.config!, problemReporter, TaskConfig.TaskConfigSource.TasksJson, this.contextKeyService); + return ProblemMatcherRegistry.onReady().then(async (): Promise<IWorkspaceFolderTaskResult> => { + const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); + const problemReporter = new ProblemReporter(this._outputChannel); + const parseResult = TaskConfig.parse(workspaceFolder, undefined, taskSystemInfo ? taskSystemInfo.platform : Platform.platform, workspaceFolderConfiguration.config!, problemReporter, TaskConfig.TaskConfigSource.TasksJson, this._contextKeyService); let hasErrors = false; if (!parseResult.validationStatus.isOK() && (parseResult.validationStatus.state !== ValidationState.Info)) { hasErrors = true; - this.showOutput(runSource); + this._showOutput(runSource); } if (problemReporter.status.isFatal()) { problemReporter.fatal(nls.localize('TaskSystem.configurationErrors', 'Error: the provided task configuration has validation errors and can\'t not be used. Please correct the errors first.')); @@ -2152,23 +2154,23 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer customizedTasks = { byIdentifier: Object.create(null) }; - for (let task of parseResult.configured) { + for (const task of parseResult.configured) { customizedTasks.byIdentifier[task.configures._key] = task; } } - if (!this.jsonTasksSupported && (parseResult.custom.length > 0)) { + if (!this._jsonTasksSupported && (parseResult.custom.length > 0)) { console.warn('Custom workspace tasks are not supported.'); } - return { workspaceFolder, set: { tasks: this.jsonTasksSupported ? parseResult.custom : [] }, configurations: customizedTasks, hasErrors }; + return { workspaceFolder, set: { tasks: this._jsonTasksSupported ? parseResult.custom : [] }, configurations: customizedTasks, hasErrors }; }); }); } - private testParseExternalConfig(config: TaskConfig.ExternalTaskRunnerConfiguration | undefined, location: string): { config: TaskConfig.ExternalTaskRunnerConfiguration | undefined; hasParseErrors: boolean } { + private _testParseExternalConfig(config: TaskConfig.IExternalTaskRunnerConfiguration | undefined, location: string): { config: TaskConfig.IExternalTaskRunnerConfiguration | undefined; hasParseErrors: boolean } { if (!config) { return { config: undefined, hasParseErrors: false }; } - let parseErrors: string[] = (config as any).$parseErrors; + const parseErrors: string[] = (config as any).$parseErrors; if (parseErrors) { let isAffected = false; for (const parseError of parseErrors) { @@ -2179,67 +2181,67 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } if (isAffected) { this._outputChannel.append(nls.localize({ key: 'TaskSystem.invalidTaskJsonOther', comment: ['Message notifies of an error in one of several places there is tasks related json, not necessarily in a file named tasks.json'] }, 'Error: The content of the tasks json in {0} has syntax errors. Please correct them before executing a task.\n', location)); - this.showOutput(); + this._showOutput(); return { config, hasParseErrors: true }; } } return { config, hasParseErrors: false }; } - private async computeWorkspaceFileTasks(workspaceFolder: IWorkspaceFolder, runSource: TaskRunSource = TaskRunSource.User): Promise<WorkspaceFolderTaskResult> { - if (this.executionEngine === ExecutionEngine.Process) { - return this.emptyWorkspaceTaskResults(workspaceFolder); + private async _computeWorkspaceFileTasks(workspaceFolder: IWorkspaceFolder, runSource: TaskRunSource = TaskRunSource.User): Promise<IWorkspaceFolderTaskResult> { + if (this._executionEngine === ExecutionEngine.Process) { + return this._emptyWorkspaceTaskResults(workspaceFolder); } - const workspaceFileConfig = this.getConfiguration(workspaceFolder, TaskSourceKind.WorkspaceFile); - const configuration = this.testParseExternalConfig(workspaceFileConfig.config, nls.localize('TasksSystem.locationWorkspaceConfig', 'workspace file')); - let customizedTasks: { byIdentifier: IStringDictionary<ConfiguringTask> } = { + const workspaceFileConfig = this._getConfiguration(workspaceFolder, TaskSourceKind.WorkspaceFile); + const configuration = this._testParseExternalConfig(workspaceFileConfig.config, nls.localize('TasksSystem.locationWorkspaceConfig', 'workspace file')); + const customizedTasks: { byIdentifier: IStringDictionary<ConfiguringTask> } = { byIdentifier: Object.create(null) }; const custom: CustomTask[] = []; - await this.computeTasksForSingleConfig(workspaceFolder, configuration.config, runSource, custom, customizedTasks.byIdentifier, TaskConfig.TaskConfigSource.WorkspaceFile); + await this._computeTasksForSingleConfig(workspaceFolder, configuration.config, runSource, custom, customizedTasks.byIdentifier, TaskConfig.TaskConfigSource.WorkspaceFile); const engine = configuration.config ? TaskConfig.ExecutionEngine.from(configuration.config) : ExecutionEngine.Terminal; if (engine === ExecutionEngine.Process) { - this.notificationService.warn(nls.localize('TaskSystem.versionWorkspaceFile', 'Only tasks version 2.0.0 permitted in workspace configuration files.')); - return this.emptyWorkspaceTaskResults(workspaceFolder); + this._notificationService.warn(nls.localize('TaskSystem.versionWorkspaceFile', 'Only tasks version 2.0.0 permitted in workspace configuration files.')); + return this._emptyWorkspaceTaskResults(workspaceFolder); } return { workspaceFolder, set: { tasks: custom }, configurations: customizedTasks, hasErrors: configuration.hasParseErrors }; } - private async computeUserTasks(workspaceFolder: IWorkspaceFolder, runSource: TaskRunSource = TaskRunSource.User): Promise<WorkspaceFolderTaskResult> { - if (this.executionEngine === ExecutionEngine.Process) { - return this.emptyWorkspaceTaskResults(workspaceFolder); + private async _computeUserTasks(workspaceFolder: IWorkspaceFolder, runSource: TaskRunSource = TaskRunSource.User): Promise<IWorkspaceFolderTaskResult> { + if (this._executionEngine === ExecutionEngine.Process) { + return this._emptyWorkspaceTaskResults(workspaceFolder); } - const userTasksConfig = this.getConfiguration(workspaceFolder, TaskSourceKind.User); - const configuration = this.testParseExternalConfig(userTasksConfig.config, nls.localize('TasksSystem.locationUserConfig', 'user settings')); - let customizedTasks: { byIdentifier: IStringDictionary<ConfiguringTask> } = { + const userTasksConfig = this._getConfiguration(workspaceFolder, TaskSourceKind.User); + const configuration = this._testParseExternalConfig(userTasksConfig.config, nls.localize('TasksSystem.locationUserConfig', 'user settings')); + const customizedTasks: { byIdentifier: IStringDictionary<ConfiguringTask> } = { byIdentifier: Object.create(null) }; const custom: CustomTask[] = []; - await this.computeTasksForSingleConfig(workspaceFolder, configuration.config, runSource, custom, customizedTasks.byIdentifier, TaskConfig.TaskConfigSource.User); + await this._computeTasksForSingleConfig(workspaceFolder, configuration.config, runSource, custom, customizedTasks.byIdentifier, TaskConfig.TaskConfigSource.User); const engine = configuration.config ? TaskConfig.ExecutionEngine.from(configuration.config) : ExecutionEngine.Terminal; if (engine === ExecutionEngine.Process) { - this.notificationService.warn(nls.localize('TaskSystem.versionSettings', 'Only tasks version 2.0.0 permitted in user settings.')); - return this.emptyWorkspaceTaskResults(workspaceFolder); + this._notificationService.warn(nls.localize('TaskSystem.versionSettings', 'Only tasks version 2.0.0 permitted in user settings.')); + return this._emptyWorkspaceTaskResults(workspaceFolder); } return { workspaceFolder, set: { tasks: custom }, configurations: customizedTasks, hasErrors: configuration.hasParseErrors }; } - private emptyWorkspaceTaskResults(workspaceFolder: IWorkspaceFolder): WorkspaceFolderTaskResult { + private _emptyWorkspaceTaskResults(workspaceFolder: IWorkspaceFolder): IWorkspaceFolderTaskResult { return { workspaceFolder, set: undefined, configurations: undefined, hasErrors: false }; } - private async computeTasksForSingleConfig(workspaceFolder: IWorkspaceFolder, config: TaskConfig.ExternalTaskRunnerConfiguration | undefined, runSource: TaskRunSource, custom: CustomTask[], customized: IStringDictionary<ConfiguringTask>, source: TaskConfig.TaskConfigSource, isRecentTask: boolean = false): Promise<boolean> { + private async _computeTasksForSingleConfig(workspaceFolder: IWorkspaceFolder, config: TaskConfig.IExternalTaskRunnerConfiguration | undefined, runSource: TaskRunSource, custom: CustomTask[], customized: IStringDictionary<ConfiguringTask>, source: TaskConfig.TaskConfigSource, isRecentTask: boolean = false): Promise<boolean> { if (!config) { return false; } - let taskSystemInfo: TaskSystemInfo | undefined = workspaceFolder ? this.getTaskSystemInfo(workspaceFolder.uri.scheme) : undefined; - let problemReporter = new ProblemReporter(this._outputChannel); - let parseResult = TaskConfig.parse(workspaceFolder, this._workspace, taskSystemInfo ? taskSystemInfo.platform : Platform.platform, config, problemReporter, source, this.contextKeyService, isRecentTask); + const taskSystemInfo: ITaskSystemInfo | undefined = workspaceFolder ? this._getTaskSystemInfo(workspaceFolder.uri.scheme) : undefined; + const problemReporter = new ProblemReporter(this._outputChannel); + const parseResult = TaskConfig.parse(workspaceFolder, this._workspace, taskSystemInfo ? taskSystemInfo.platform : Platform.platform, config, problemReporter, source, this._contextKeyService, isRecentTask); let hasErrors = false; if (!parseResult.validationStatus.isOK() && (parseResult.validationStatus.state !== ValidationState.Info)) { - this.showOutput(runSource); + this._showOutput(runSource); hasErrors = true; } if (problemReporter.status.isFatal()) { @@ -2247,37 +2249,37 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return hasErrors; } if (parseResult.configured && parseResult.configured.length > 0) { - for (let task of parseResult.configured) { + for (const task of parseResult.configured) { customized[task.configures._key] = task; } } - if (!this.jsonTasksSupported && (parseResult.custom.length > 0)) { + if (!this._jsonTasksSupported && (parseResult.custom.length > 0)) { console.warn('Custom workspace tasks are not supported.'); } else { - for (let task of parseResult.custom) { + for (const task of parseResult.custom) { custom.push(task); } } return hasErrors; } - private computeConfiguration(workspaceFolder: IWorkspaceFolder): Promise<WorkspaceFolderConfigurationResult> { - let { config, hasParseErrors } = this.getConfiguration(workspaceFolder); - return Promise.resolve<WorkspaceFolderConfigurationResult>({ workspaceFolder, config, hasErrors: hasParseErrors }); + private _computeConfiguration(workspaceFolder: IWorkspaceFolder): Promise<IWorkspaceFolderConfigurationResult> { + const { config, hasParseErrors } = this._getConfiguration(workspaceFolder); + return Promise.resolve<IWorkspaceFolderConfigurationResult>({ workspaceFolder, config, hasErrors: hasParseErrors }); } - protected abstract computeLegacyConfiguration(workspaceFolder: IWorkspaceFolder): Promise<WorkspaceFolderConfigurationResult>; + protected abstract _computeLegacyConfiguration(workspaceFolder: IWorkspaceFolder): Promise<IWorkspaceFolderConfigurationResult>; - private computeWorkspaceFolderSetup(): [IWorkspaceFolder[], IWorkspaceFolder[], ExecutionEngine, JsonSchemaVersion, IWorkspace | undefined] { - let workspaceFolders: IWorkspaceFolder[] = []; - let ignoredWorkspaceFolders: IWorkspaceFolder[] = []; + private _computeWorkspaceFolderSetup(): [IWorkspaceFolder[], IWorkspaceFolder[], ExecutionEngine, JsonSchemaVersion, IWorkspace | undefined] { + const workspaceFolders: IWorkspaceFolder[] = []; + const ignoredWorkspaceFolders: IWorkspaceFolder[] = []; let executionEngine = ExecutionEngine.Terminal; let schemaVersion = JsonSchemaVersion.V2_0_0; let workspace: IWorkspace | undefined; - if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { - let workspaceFolder: IWorkspaceFolder = this.contextService.getWorkspace().folders[0]; + if (this._contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + const workspaceFolder: IWorkspaceFolder = this._contextService.getWorkspace().folders[0]; workspaceFolders.push(workspaceFolder); - executionEngine = this.computeExecutionEngine(workspaceFolder); + executionEngine = this._computeExecutionEngine(workspaceFolder); const telemetryData: { [key: string]: any } = { executionEngineVersion: executionEngine }; @@ -2288,12 +2290,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer "executionEngineVersion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The engine version of tasks." } } */ - this.telemetryService.publicLog('taskService.engineVersion', telemetryData); - schemaVersion = this.computeJsonSchemaVersion(workspaceFolder); - } else if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - workspace = this.contextService.getWorkspace(); - for (let workspaceFolder of this.contextService.getWorkspace().folders) { - if (schemaVersion === this.computeJsonSchemaVersion(workspaceFolder)) { + this._telemetryService.publicLog('taskService.engineVersion', telemetryData); + schemaVersion = this._computeJsonSchemaVersion(workspaceFolder); + } else if (this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + workspace = this._contextService.getWorkspace(); + for (const workspaceFolder of this._contextService.getWorkspace().folders) { + if (schemaVersion === this._computeJsonSchemaVersion(workspaceFolder)) { workspaceFolders.push(workspaceFolder); } else { ignoredWorkspaceFolders.push(workspaceFolder); @@ -2307,28 +2309,28 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return [workspaceFolders, ignoredWorkspaceFolders, executionEngine, schemaVersion, workspace]; } - private computeExecutionEngine(workspaceFolder: IWorkspaceFolder): ExecutionEngine { - let { config } = this.getConfiguration(workspaceFolder); + private _computeExecutionEngine(workspaceFolder: IWorkspaceFolder): ExecutionEngine { + const { config } = this._getConfiguration(workspaceFolder); if (!config) { return ExecutionEngine._default; } return TaskConfig.ExecutionEngine.from(config); } - private computeJsonSchemaVersion(workspaceFolder: IWorkspaceFolder): JsonSchemaVersion { - let { config } = this.getConfiguration(workspaceFolder); + private _computeJsonSchemaVersion(workspaceFolder: IWorkspaceFolder): JsonSchemaVersion { + const { config } = this._getConfiguration(workspaceFolder); if (!config) { return JsonSchemaVersion.V2_0_0; } return TaskConfig.JsonSchemaVersion.from(config); } - protected getConfiguration(workspaceFolder: IWorkspaceFolder, source?: string): { config: TaskConfig.ExternalTaskRunnerConfiguration | undefined; hasParseErrors: boolean } { + protected _getConfiguration(workspaceFolder: IWorkspaceFolder, source?: string): { config: TaskConfig.IExternalTaskRunnerConfiguration | undefined; hasParseErrors: boolean } { let result; - if ((source !== TaskSourceKind.User) && (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY)) { + if ((source !== TaskSourceKind.User) && (this._contextService.getWorkbenchState() === WorkbenchState.EMPTY)) { result = undefined; } else { - const wholeConfig = this.configurationService.inspect<TaskConfig.ExternalTaskRunnerConfiguration>('tasks', { resource: workspaceFolder.uri }); + const wholeConfig = this._configurationService.inspect<TaskConfig.IExternalTaskRunnerConfiguration>('tasks', { resource: workspaceFolder.uri }); switch (source) { case TaskSourceKind.User: { if (wholeConfig.userValue !== wholeConfig.workspaceFolderValue) { @@ -2338,7 +2340,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } case TaskSourceKind.Workspace: result = Objects.deepClone(wholeConfig.workspaceFolderValue); break; case TaskSourceKind.WorkspaceFile: { - if ((this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) + if ((this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) && (wholeConfig.workspaceFolderValue !== wholeConfig.workspaceValue)) { result = Objects.deepClone(wholeConfig.workspaceValue); } @@ -2350,7 +2352,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!result) { return { config: undefined, hasParseErrors: false }; } - let parseErrors: string[] = (result as any).$parseErrors; + const parseErrors: string[] = (result as any).$parseErrors; if (parseErrors) { let isAffected = false; for (const parseError of parseErrors) { @@ -2361,7 +2363,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } if (isAffected) { this._outputChannel.append(nls.localize('TaskSystem.invalidTaskJson', 'Error: The content of the tasks.json file has syntax errors. Please correct them before executing a task.\n')); - this.showOutput(); + this._showOutput(); return { config: undefined, hasParseErrors: true }; } } @@ -2372,67 +2374,67 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (this._taskSystem) { return this._taskSystem instanceof TerminalTaskSystem; } - return this.executionEngine === ExecutionEngine.Terminal; + return this._executionEngine === ExecutionEngine.Terminal; } public configureAction(): Action { const thisCapture: AbstractTaskService = this; return new class extends Action { constructor() { - super(ConfigureTaskAction.ID, ConfigureTaskAction.TEXT, undefined, true, () => { thisCapture.runConfigureTasks(); return Promise.resolve(undefined); }); + super(ConfigureTaskAction.ID, ConfigureTaskAction.TEXT, undefined, true, () => { thisCapture._runConfigureTasks(); return Promise.resolve(undefined); }); } }; } - private handleError(err: any): void { + private _handleError(err: any): void { let showOutput = true; if (err instanceof TaskError) { - let buildError = <TaskError>err; - let needsConfig = buildError.code === TaskErrors.NotConfigured || buildError.code === TaskErrors.NoBuildTask || buildError.code === TaskErrors.NoTestTask; - let needsTerminate = buildError.code === TaskErrors.RunningTask; + const buildError = <TaskError>err; + const needsConfig = buildError.code === TaskErrors.NotConfigured || buildError.code === TaskErrors.NoBuildTask || buildError.code === TaskErrors.NoTestTask; + const needsTerminate = buildError.code === TaskErrors.RunningTask; if (needsConfig || needsTerminate) { - this.notificationService.prompt(buildError.severity, buildError.message, [{ + this._notificationService.prompt(buildError.severity, buildError.message, [{ label: needsConfig ? ConfigureTaskAction.TEXT : nls.localize('TerminateAction.label', "Terminate Task"), run: () => { if (needsConfig) { - this.runConfigureTasks(); + this._runConfigureTasks(); } else { - this.runTerminateCommand(); + this._runTerminateCommand(); } } }]); } else { - this.notificationService.notify({ severity: buildError.severity, message: buildError.message }); + this._notificationService.notify({ severity: buildError.severity, message: buildError.message }); } } else if (err instanceof Error) { - let error = <Error>err; - this.notificationService.error(error.message); + const error = <Error>err; + this._notificationService.error(error.message); showOutput = false; } else if (Types.isString(err)) { - this.notificationService.error(<string>err); + this._notificationService.error(<string>err); } else { - this.notificationService.error(nls.localize('TaskSystem.unknownError', 'An error has occurred while running a task. See task log for details.')); + this._notificationService.error(nls.localize('TaskSystem.unknownError', 'An error has occurred while running a task. See task log for details.')); } if (showOutput) { - this.showOutput(); + this._showOutput(); } } - private canRunCommand(): boolean { + private _canRunCommand(): boolean { return true; } - private showDetail(): boolean { - return this.configurationService.getValue<boolean>(QUICKOPEN_DETAIL_CONFIG); + private _showDetail(): boolean { + return this._configurationService.getValue<boolean>(QUICKOPEN_DETAIL_CONFIG); } - private async createTaskQuickPickEntries(tasks: Task[], group: boolean = false, sort: boolean = false, selectedEntry?: TaskQuickPickEntry, includeRecents: boolean = true): Promise<TaskQuickPickEntry[]> { - let encounteredTasks: { [key: string]: TaskQuickPickEntry[] } = {}; + private async _createTaskQuickPickEntries(tasks: Task[], group: boolean = false, sort: boolean = false, selectedEntry?: ITaskQuickPickEntry, includeRecents: boolean = true): Promise<ITaskQuickPickEntry[]> { + let encounteredTasks: { [key: string]: ITaskQuickPickEntry[] } = {}; if (tasks === undefined || tasks === null || tasks.length === 0) { return []; } - const TaskQuickPickEntry = (task: Task): TaskQuickPickEntry => { - const newEntry = { label: task._label, description: this.getTaskDescription(task), task, detail: this.showDetail() ? task.configurationProperties.detail : undefined }; + const TaskQuickPickEntry = (task: Task): ITaskQuickPickEntry => { + const newEntry = { label: task._label, description: this.getTaskDescription(task), task, detail: this._showDetail() ? task.configurationProperties.detail : undefined }; if (encounteredTasks[task._id]) { if (encounteredTasks[task._id].length === 1) { encounteredTasks[task._id][0].label += ' (1)'; @@ -2445,12 +2447,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return newEntry; }; - function fillEntries(entries: QuickPickInput<TaskQuickPickEntry>[], tasks: Task[], groupLabel: string): void { + function fillEntries(entries: QuickPickInput<ITaskQuickPickEntry>[], tasks: Task[], groupLabel: string): void { if (tasks.length) { entries.push({ type: 'separator', label: groupLabel }); } - for (let task of tasks) { - let entry: TaskQuickPickEntry = TaskQuickPickEntry(task); + for (const task of tasks) { + const entry: ITaskQuickPickEntry = TaskQuickPickEntry(task); entry.buttons = [{ iconClass: ThemeIcon.asClassName(configureTaskIcon), tooltip: nls.localize('configureTask', "Configure Task") }]; if (selectedEntry && (task === selectedEntry.task)) { entries.unshift(selectedEntry); @@ -2459,20 +2461,20 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } } - let entries: TaskQuickPickEntry[]; + let entries: ITaskQuickPickEntry[]; if (group) { entries = []; if (tasks.length === 1) { entries.push(TaskQuickPickEntry(tasks[0])); } else { - let recentlyUsedTasks = await this.readRecentTasks(); - let recent: Task[] = []; - let recentSet: Set<string> = new Set(); + const recentlyUsedTasks = await this.readRecentTasks(); + const recent: Task[] = []; + const recentSet: Set<string> = new Set(); let configured: Task[] = []; let detected: Task[] = []; - let taskMap: IStringDictionary<Task> = Object.create(null); + const taskMap: IStringDictionary<Task> = Object.create(null); tasks.forEach(task => { - let key = task.getCommonTaskId(); + const key = task.getCommonTaskId(); if (key) { taskMap[key] = task; } @@ -2481,14 +2483,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const key = recentTask.getCommonTaskId(); if (key) { recentSet.add(key); - let task = taskMap[key]; + const task = taskMap[key]; if (task) { recent.push(task); } } }); - for (let task of tasks) { - let key = task.getCommonTaskId(); + for (const task of tasks) { + const key = task.getCommonTaskId(); if (!key || !recentSet.has(key)) { if ((task._source.kind === TaskSourceKind.Workspace) || (task._source.kind === TaskSourceKind.User)) { configured.push(task); @@ -2511,29 +2513,29 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const sorter = this.createSorter(); tasks = tasks.sort((a, b) => sorter.compare(a, b)); } - entries = tasks.map<TaskQuickPickEntry>(task => TaskQuickPickEntry(task)); + entries = tasks.map<ITaskQuickPickEntry>(task => TaskQuickPickEntry(task)); } encounteredTasks = {}; return entries; } - private async showTwoLevelQuickPick(placeHolder: string, defaultEntry?: TaskQuickPickEntry) { - return TaskQuickPick.show(this, this.configurationService, this.quickInputService, this.notificationService, this.dialogService, placeHolder, defaultEntry); + private async _showTwoLevelQuickPick(placeHolder: string, defaultEntry?: ITaskQuickPickEntry) { + return TaskQuickPick.show(this, this._configurationService, this._quickInputService, this._notificationService, this._dialogService, this._themeService, placeHolder, defaultEntry); } - private async showQuickPick(tasks: Promise<Task[]> | Task[], placeHolder: string, defaultEntry?: TaskQuickPickEntry, group: boolean = false, sort: boolean = false, selectedEntry?: TaskQuickPickEntry, additionalEntries?: TaskQuickPickEntry[]): Promise<TaskQuickPickEntry | undefined | null> { + private async _showQuickPick(tasks: Promise<Task[]> | Task[], placeHolder: string, defaultEntry?: ITaskQuickPickEntry, group: boolean = false, sort: boolean = false, selectedEntry?: ITaskQuickPickEntry, additionalEntries?: ITaskQuickPickEntry[]): Promise<ITaskQuickPickEntry | undefined | null> { const tokenSource = new CancellationTokenSource(); const cancellationToken: CancellationToken = tokenSource.token; - let _createEntries = new Promise<QuickPickInput<TaskQuickPickEntry>[]>((resolve) => { + const createEntries = new Promise<QuickPickInput<ITaskQuickPickEntry>[]>((resolve) => { if (Array.isArray(tasks)) { - resolve(this.createTaskQuickPickEntries(tasks, group, sort, selectedEntry)); + resolve(this._createTaskQuickPickEntries(tasks, group, sort, selectedEntry)); } else { - resolve(tasks.then((tasks) => this.createTaskQuickPickEntries(tasks, group, sort, selectedEntry))); + resolve(tasks.then((tasks) => this._createTaskQuickPickEntries(tasks, group, sort, selectedEntry))); } }); const timeout: boolean = await Promise.race([new Promise<boolean>((resolve) => { - _createEntries.then(() => resolve(false)); + createEntries.then(() => resolve(false)); }), new Promise<boolean>((resolve) => { const timer = setTimeout(() => { clearTimeout(timer); @@ -2541,12 +2543,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }, 200); })]); - if (!timeout && ((await _createEntries).length === 1) && this.configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) { - return (<TaskQuickPickEntry>(await _createEntries)[0]); + if (!timeout && ((await createEntries).length === 1) && this._configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) { + return (<ITaskQuickPickEntry>(await createEntries)[0]); } - const pickEntries = _createEntries.then((entries) => { - if ((entries.length === 1) && this.configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) { + const pickEntries = createEntries.then((entries) => { + if ((entries.length === 1) && this._configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) { tokenSource.cancel(); } else if ((entries.length === 0) && defaultEntry) { entries.push(defaultEntry); @@ -2557,13 +2559,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return entries; }); - const picker: IQuickPick<TaskQuickPickEntry> = this.quickInputService.createQuickPick(); + const picker: IQuickPick<ITaskQuickPickEntry> = this._quickInputService.createQuickPick(); picker.placeholder = placeHolder; picker.matchOnDescription = true; picker.onDidTriggerItemButton(context => { - let task = context.item.task; - this.quickInputService.cancel(); + const task = context.item.task; + this._quickInputService.cancel(); if (ContributedTask.is(task)) { this.customize(task, undefined, true); } else if (CustomTask.is(task)) { @@ -2577,14 +2579,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); picker.show(); - return new Promise<TaskQuickPickEntry | undefined | null>(resolve => { + return new Promise<ITaskQuickPickEntry | undefined | null>(resolve => { this._register(picker.onDidAccept(async () => { let selection = picker.selectedItems ? picker.selectedItems[0] : undefined; if (cancellationToken.isCancellationRequested) { // canceled when there's only one task const task = (await pickEntries)[0]; if ((<any>task).task) { - selection = <TaskQuickPickEntry>task; + selection = <ITaskQuickPickEntry>task; } } picker.dispose(); @@ -2596,45 +2598,45 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - private needsRecentTasksMigration(): boolean { - return (this.getRecentlyUsedTasksV1().size > 0) && (this.getRecentlyUsedTasks().size === 0); + private _needsRecentTasksMigration(): boolean { + return (this.getRecentlyUsedTasksV1().size > 0) && (this._getRecentlyUsedTasks().size === 0); } - private async migrateRecentTasks(tasks: Task[]) { - if (!this.needsRecentTasksMigration()) { + private async _migrateRecentTasks(tasks: Task[]) { + if (!this._needsRecentTasksMigration()) { return; } - let recentlyUsedTasks = this.getRecentlyUsedTasksV1(); - let taskMap: IStringDictionary<Task> = Object.create(null); + const recentlyUsedTasks = this.getRecentlyUsedTasksV1(); + const taskMap: IStringDictionary<Task> = Object.create(null); tasks.forEach(task => { - let key = task.getRecentlyUsedKey(); + const key = task.getRecentlyUsedKey(); if (key) { taskMap[key] = task; } }); const reversed = [...recentlyUsedTasks.keys()].reverse(); for (const key in reversed) { - let task = taskMap[key]; + const task = taskMap[key]; if (task) { - await this.setRecentlyUsedTask(task); + await this._setRecentlyUsedTask(task); } } - this.storageService.remove(AbstractTaskService.RecentlyUsedTasks_Key, StorageScope.WORKSPACE); + this._storageService.remove(AbstractTaskService.RecentlyUsedTasks_Key, StorageScope.WORKSPACE); } - private showIgnoredFoldersMessage(): Promise<void> { + private _showIgnoredFoldersMessage(): Promise<void> { if (this.ignoredWorkspaceFolders.length === 0 || !this.showIgnoreMessage) { return Promise.resolve(undefined); } - this.notificationService.prompt( + this._notificationService.prompt( Severity.Info, nls.localize('TaskService.ignoredFolder', 'The following workspace folders are ignored since they use task version 0.1.0: {0}', this.ignoredWorkspaceFolders.map(f => f.name).join(', ')), [{ label: nls.localize('TaskService.notAgain', "Don't Show Again"), isSecondary: true, run: () => { - this.storageService.store(AbstractTaskService.IgnoreTask010DonotShowAgain_key, true, StorageScope.WORKSPACE, StorageTarget.USER); + this._storageService.store(AbstractTaskService.IgnoreTask010DonotShowAgain_key, true, StorageScope.WORKSPACE, StorageTarget.USER); this._showIgnoreMessage = false; } }] @@ -2643,28 +2645,28 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return Promise.resolve(undefined); } - private async trust(): Promise<boolean> { - return (await this.workspaceTrustRequestService.requestWorkspaceTrust( + private async _trust(): Promise<boolean> { + return (await this._workspaceTrustRequestService.requestWorkspaceTrust( { message: nls.localize('TaskService.requestTrust', "Listing and running tasks requires that some of the files in this workspace be executed as code.") })) === true; } - private runTaskCommand(arg?: any): void { - if (!this.canRunCommand()) { + private _runTaskCommand(arg?: any): void { + if (!this._canRunCommand()) { return; } - let identifier = this.getTaskIdentifier(arg); + const identifier = this._getTaskIdentifier(arg); if (identifier !== undefined) { - this.getGroupedTasks().then(async (grouped) => { - let resolver = this.createResolver(grouped); - let folderURIs: (URI | string)[] = this.contextService.getWorkspace().folders.map(folder => folder.uri); - if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - folderURIs.push(this.contextService.getWorkspace().configuration!); + this._getGroupedTasks().then(async (grouped) => { + const resolver = this._createResolver(grouped); + const folderURIs: (URI | string)[] = this._contextService.getWorkspace().folders.map(folder => folder.uri); + if (this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + folderURIs.push(this._contextService.getWorkspace().configuration!); } folderURIs.push(USER_TASKS_GROUP_KEY); - for (let uri of folderURIs) { - let task = await resolver.resolve(uri, identifier); + for (const uri of folderURIs) { + const task = await resolver.resolve(uri, identifier); if (task) { this.run(task).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here @@ -2672,34 +2674,34 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return; } } - this.doRunTaskCommand(grouped.all()); + this._doRunTaskCommand(grouped.all()); }, () => { - this.doRunTaskCommand(); + this._doRunTaskCommand(); }); } else { - this.doRunTaskCommand(); + this._doRunTaskCommand(); } } - private tasksAndGroupedTasks(filter?: TaskFilter): { tasks: Promise<Task[]>; grouped: Promise<TaskMap> } { - if (!this.versionAndEngineCompatible(filter)) { + private _tasksAndGroupedTasks(filter?: ITaskFilter): { tasks: Promise<Task[]>; grouped: Promise<TaskMap> } { + if (!this._versionAndEngineCompatible(filter)) { return { tasks: Promise.resolve<Task[]>([]), grouped: Promise.resolve(new TaskMap()) }; } - const grouped = this.getGroupedTasks(filter ? filter.type : undefined); + const grouped = this._getGroupedTasks(filter ? filter.type : undefined); const tasks = grouped.then((map) => { if (!filter || !filter.type) { return map.all(); } - let result: Task[] = []; + const result: Task[] = []; map.forEach((tasks) => { - for (let task of tasks) { + for (const task of tasks) { if (ContributedTask.is(task) && task.defines.type === filter.type) { result.push(task); } else if (CustomTask.is(task)) { if (task.type === filter.type) { result.push(task); } else { - let customizes = task.customizes(); + const customizes = task.customizes(); if (customizes && customizes.type === filter.type) { result.push(task); } @@ -2712,13 +2714,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return { tasks, grouped }; } - private doRunTaskCommand(tasks?: Task[]): void { + private _doRunTaskCommand(tasks?: Task[]): void { const pickThen = (task: Task | undefined | null) => { if (task === undefined) { return; } if (task === null) { - this.runConfigureTasks(); + this._runConfigureTasks(); } else { this.run(task, { attachProblemMatcher: true }, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here @@ -2728,13 +2730,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const placeholder = nls.localize('TaskService.pickRunTask', 'Select the task to run'); - this.showIgnoredFoldersMessage().then(() => { - if (this.configurationService.getValue(USE_SLOW_PICKER)) { + this._showIgnoredFoldersMessage().then(() => { + if (this._configurationService.getValue(USE_SLOW_PICKER)) { let taskResult: { tasks: Promise<Task[]>; grouped: Promise<TaskMap> } | undefined = undefined; if (!tasks) { - taskResult = this.tasksAndGroupedTasks(); + taskResult = this._tasksAndGroupedTasks(); } - this.showQuickPick(tasks ? tasks : taskResult!.tasks, placeholder, + this._showQuickPick(tasks ? tasks : taskResult!.tasks, placeholder, { label: nls.localize('TaskService.noEntryToRunSlow', '$(plus) Configure a Task'), task: null @@ -2744,7 +2746,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return pickThen(entry ? entry.task : undefined); }); } else { - this.showTwoLevelQuickPick(placeholder, + this._showTwoLevelQuickPick(placeholder, { label: nls.localize('TaskService.noEntryToRun', '$(plus) Configure a Task'), task: null @@ -2754,18 +2756,18 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - private reRunTaskCommand(): void { - if (!this.canRunCommand()) { + private _reRunTaskCommand(): void { + if (!this._canRunCommand()) { return; } ProblemMatcherRegistry.onReady().then(() => { - return this.editorService.saveAll({ reason: SaveReason.AUTO }).then(() => { // make sure all dirty editors are saved - let executeResult = this.getTaskSystem().rerun(); + return this._editorService.saveAll({ reason: SaveReason.AUTO }).then(() => { // make sure all dirty editors are saved + const executeResult = this._getTaskSystem().rerun(); if (executeResult) { - return this.handleExecuteResult(executeResult); + return this._handleExecuteResult(executeResult); } else { - this.doRunTaskCommand(); + this._doRunTaskCommand(); return Promise.resolve(undefined); } }); @@ -2779,10 +2781,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer * @param taskGlobsInList - This tells splitPerGroupType to filter out globbed tasks (into default), otherwise fall back to boolean * @returns */ - private splitPerGroupType(tasks: Task[], taskGlobsInList: boolean = false): { none: Task[]; defaults: Task[] } { - let none: Task[] = []; - let defaults: Task[] = []; - for (let task of tasks) { + private _splitPerGroupType(tasks: Task[], taskGlobsInList: boolean = false): { none: Task[]; defaults: Task[] } { + const none: Task[] = []; + const defaults: Task[] = []; + for (const task of tasks) { // At this point (assuming taskGlobsInList is true) there are tasks with matching globs, so only put those in defaults if (taskGlobsInList && typeof (task.configurationProperties.group as TaskGroup).isDefault === 'string') { defaults.push(task); @@ -2795,42 +2797,42 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return { none, defaults }; } - private runTaskGroupCommand(taskGroup: TaskGroup, strings: { + private _runTaskGroupCommand(taskGroup: TaskGroup, strings: { fetching: string; select: string; notFoundConfigure: string; }, configure: () => void, legacyCommand: () => void): void { - if (!this.canRunCommand()) { + if (!this._canRunCommand()) { return; } if (this.schemaVersion === JsonSchemaVersion.V0_1_0) { legacyCommand(); return; } - let options: IProgressOptions = { + const options: IProgressOptions = { location: ProgressLocation.Window, title: strings.fetching }; - let promise = (async () => { + const promise = (async () => { let taskGroupTasks: (Task | ConfiguringTask)[] = []; - async function runSingleTask(task: Task | undefined, problemMatcherOptions: ProblemMatcherRunOptions | undefined, that: AbstractTaskService) { + async function runSingleTask(task: Task | undefined, problemMatcherOptions: IProblemMatcherRunOptions | undefined, that: AbstractTaskService) { that.run(task, problemMatcherOptions, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); } const chooseAndRunTask = (tasks: Task[]) => { - this.showIgnoredFoldersMessage().then(() => { - this.showQuickPick(tasks, + this._showIgnoredFoldersMessage().then(() => { + this._showQuickPick(tasks, strings.select, { label: strings.notFoundConfigure, task: null }, true).then((entry) => { - let task: Task | undefined | null = entry ? entry.task : undefined; + const task: Task | undefined | null = entry ? entry.task : undefined; if (task === undefined) { return; } @@ -2844,9 +2846,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }; // First check for globs before checking for the default tasks of the task group - const absoluteURI = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); + const absoluteURI = EditorResourceAccessor.getOriginalUri(this._editorService.activeEditor); if (absoluteURI) { - const workspaceFolder = this.contextService.getWorkspaceFolder(absoluteURI); + const workspaceFolder = this._contextService.getWorkspaceFolder(absoluteURI); // fallback to absolute path of the file if it is not in a workspace or relative path cannot be found const relativePath = workspaceFolder?.uri ? (resources.relativePath(workspaceFolder.uri, absoluteURI) ?? absoluteURI.path) : absoluteURI.path; @@ -2861,11 +2863,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } const handleMultipleTasks = (areGlobTasks: boolean) => { - return this.getTasksForGroup(taskGroup).then((tasks) => { + return this._getTasksForGroup(taskGroup).then((tasks) => { if (tasks.length > 0) { // If we're dealing with tasks that were chosen because of a glob match, // then put globs in the defaults and everything else in none - let { none, defaults } = this.splitPerGroupType(tasks, areGlobTasks); + const { none, defaults } = this._splitPerGroupType(tasks, areGlobTasks); if (defaults.length === 1) { runSingleTask(defaults[0], undefined, this); return; @@ -2914,35 +2916,35 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer // Multiple default tasks returned, show the quickPicker return handleMultipleTasks(false); })(); - this.progressService.withProgress(options, () => promise); + this._progressService.withProgress(options, () => promise); } - private runBuildCommand(): void { - return this.runTaskGroupCommand(TaskGroup.Build, { + private _runBuildCommand(): void { + return this._runTaskGroupCommand(TaskGroup.Build, { fetching: nls.localize('TaskService.fetchingBuildTasks', 'Fetching build tasks...'), select: nls.localize('TaskService.pickBuildTask', 'Select the build task to run'), notFoundConfigure: nls.localize('TaskService.noBuildTask', 'No build task to run found. Configure Build Task...') - }, this.runConfigureDefaultBuildTask, this.build); + }, this._runConfigureDefaultBuildTask, this._build); } - private runTestCommand(): void { - return this.runTaskGroupCommand(TaskGroup.Test, { + private _runTestCommand(): void { + return this._runTaskGroupCommand(TaskGroup.Test, { fetching: nls.localize('TaskService.fetchingTestTasks', 'Fetching test tasks...'), select: nls.localize('TaskService.pickTestTask', 'Select the test task to run'), notFoundConfigure: nls.localize('TaskService.noTestTaskTerminal', 'No test task to run found. Configure Tasks...') - }, this.runConfigureDefaultTestTask, this.runTest); + }, this._runConfigureDefaultTestTask, this._runTest); } - private runTerminateCommand(arg?: any): void { - if (!this.canRunCommand()) { + private _runTerminateCommand(arg?: any): void { + if (!this._canRunCommand()) { return; } if (arg === 'terminateAll') { - this.terminateAll(); + this._terminateAll(); return; } - let runQuickPick = (promise?: Promise<Task[]>) => { - this.showQuickPick(promise || this.getActiveTasks(), + const runQuickPick = (promise?: Promise<Task[]>) => { + this._showQuickPick(promise || this.getActiveTasks(), nls.localize('TaskService.taskToTerminate', 'Select a task to terminate'), { label: nls.localize('TaskService.noTaskRunning', 'No task is currently running'), @@ -2957,9 +2959,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }] ).then(entry => { if (entry && entry.id === 'terminateAll') { - this.terminateAll(); + this._terminateAll(); } - let task: Task | undefined | null = entry ? entry.task : undefined; + const task: Task | undefined | null = entry ? entry.task : undefined; if (task === undefined || task === null) { return; } @@ -2967,12 +2969,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); }; if (this.inTerminal()) { - let identifier = this.getTaskIdentifier(arg); + const identifier = this._getTaskIdentifier(arg); let promise: Promise<Task[]>; if (identifier !== undefined) { promise = this.getActiveTasks(); promise.then((tasks) => { - for (let task of tasks) { + for (const task of tasks) { if (task.matches(identifier)) { this.terminate(task); return; @@ -2984,18 +2986,18 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer runQuickPick(); } } else { - this.isActive().then((active) => { + this._isActive().then((active) => { if (active) { - this.terminateAll().then((responses) => { + this._terminateAll().then((responses) => { // the output runner has only one task - let response = responses[0]; + const response = responses[0]; if (response.success) { return; } if (response.code && response.code === TerminateResponseCode.ProcessNotFound) { - this.notificationService.error(nls.localize('TerminateAction.noProcess', 'The launched process doesn\'t exist anymore. If the task spawned background tasks exiting VS Code might result in orphaned processes.')); + this._notificationService.error(nls.localize('TerminateAction.noProcess', 'The launched process doesn\'t exist anymore. If the task spawned background tasks exiting VS Code might result in orphaned processes.')); } else { - this.notificationService.error(nls.localize('TerminateAction.failed', 'Failed to terminate running task')); + this._notificationService.error(nls.localize('TerminateAction.failed', 'Failed to terminate running task')); } }); } @@ -3003,12 +3005,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private runRestartTaskCommand(arg?: any): void { - if (!this.canRunCommand()) { + private _runRestartTaskCommand(arg?: any): void { + if (!this._canRunCommand()) { return; } - let runQuickPick = (promise?: Promise<Task[]>) => { - this.showQuickPick(promise || this.getActiveTasks(), + const runQuickPick = (promise?: Promise<Task[]>) => { + this._showQuickPick(promise || this.getActiveTasks(), nls.localize('TaskService.taskToRestart', 'Select the task to restart'), { label: nls.localize('TaskService.noTaskToRestart', 'No task to restart'), @@ -3016,22 +3018,22 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }, false, true ).then(entry => { - let task: Task | undefined | null = entry ? entry.task : undefined; + const task: Task | undefined | null = entry ? entry.task : undefined; if (task === undefined || task === null) { return; } - this.restart(task); + this._restart(task); }); }; if (this.inTerminal()) { - let identifier = this.getTaskIdentifier(arg); + const identifier = this._getTaskIdentifier(arg); let promise: Promise<Task[]>; if (identifier !== undefined) { promise = this.getActiveTasks(); promise.then((tasks) => { - for (let task of tasks) { + for (const task of tasks) { if (task.matches(identifier)) { - this.restart(task); + this._restart(task); return; } } @@ -3045,46 +3047,46 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (activeTasks.length === 0) { return; } - let task = activeTasks[0]; - this.restart(task); + const task = activeTasks[0]; + this._restart(task); }); } } - private getTaskIdentifier(arg?: any): string | KeyedTaskIdentifier | undefined { + private _getTaskIdentifier(arg?: any): string | KeyedTaskIdentifier | undefined { let result: string | KeyedTaskIdentifier | undefined = undefined; if (Types.isString(arg)) { result = arg; - } else if (arg && Types.isString((arg as TaskIdentifier).type)) { - result = TaskDefinition.createTaskIdentifier(arg as TaskIdentifier, console); + } else if (arg && Types.isString((arg as ITaskIdentifier).type)) { + result = TaskDefinition.createTaskIdentifier(arg as ITaskIdentifier, console); } return result; } - private configHasTasks(taskConfig?: TaskConfig.ExternalTaskRunnerConfiguration): boolean { + private _configHasTasks(taskConfig?: TaskConfig.IExternalTaskRunnerConfiguration): boolean { return !!taskConfig && !!taskConfig.tasks && taskConfig.tasks.length > 0; } - private openTaskFile(resource: URI, taskSource: string) { + private _openTaskFile(resource: URI, taskSource: string) { let configFileCreated = false; - this.fileService.stat(resource).then((stat) => stat, () => undefined).then(async (stat) => { + this._fileService.stat(resource).then((stat) => stat, () => undefined).then(async (stat) => { const fileExists: boolean = !!stat; - const configValue = this.configurationService.inspect<TaskConfig.ExternalTaskRunnerConfiguration>('tasks'); + const configValue = this._configurationService.inspect<TaskConfig.IExternalTaskRunnerConfiguration>('tasks'); let tasksExistInFile: boolean; let target: ConfigurationTarget; switch (taskSource) { - case TaskSourceKind.User: tasksExistInFile = this.configHasTasks(configValue.userValue); target = ConfigurationTarget.USER; break; - case TaskSourceKind.WorkspaceFile: tasksExistInFile = this.configHasTasks(configValue.workspaceValue); target = ConfigurationTarget.WORKSPACE; break; - default: tasksExistInFile = this.configHasTasks(configValue.workspaceFolderValue); target = ConfigurationTarget.WORKSPACE_FOLDER; + case TaskSourceKind.User: tasksExistInFile = this._configHasTasks(configValue.userValue); target = ConfigurationTarget.USER; break; + case TaskSourceKind.WorkspaceFile: tasksExistInFile = this._configHasTasks(configValue.workspaceValue); target = ConfigurationTarget.WORKSPACE; break; + default: tasksExistInFile = this._configHasTasks(configValue.workspaceFolderValue); target = ConfigurationTarget.WORKSPACE_FOLDER; } let content; if (!tasksExistInFile) { - const pickTemplateResult = await this.quickInputService.pick(getTaskTemplates(), { placeHolder: nls.localize('TaskService.template', 'Select a Task Template') }); + const pickTemplateResult = await this._quickInputService.pick(getTaskTemplates(), { placeHolder: nls.localize('TaskService.template', 'Select a Task Template') }); if (!pickTemplateResult) { return Promise.resolve(undefined); } content = pickTemplateResult.content; - let editorConfig = this.configurationService.getValue() as any; + const editorConfig = this._configurationService.getValue() as any; if (editorConfig.editor.insertSpaces) { content = content.replace(/(\n)(\t+)/g, (_, s1, s2) => s1 + ' '.repeat(s2.length * editorConfig.editor.tabSize)); } @@ -3092,12 +3094,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } if (!fileExists && content) { - return this.textFileService.create([{ resource, value: content }]).then(result => { + return this._textFileService.create([{ resource, value: content }]).then(result => { return result[0].resource; }); } else if (fileExists && (tasksExistInFile || content)) { if (content) { - this.configurationService.updateValue('tasks', json.parse(content), target); + this._configurationService.updateValue('tasks', json.parse(content), target); } return stat?.resource; } @@ -3106,7 +3108,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!resource) { return; } - this.editorService.openEditor({ + this._editorService.openEditor({ resource, options: { pinned: configFileCreated // pin only if config file is created #8727 @@ -3115,17 +3117,17 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - private isTaskEntry(value: IQuickPickItem): value is IQuickPickItem & { task: Task } { - let candidate: IQuickPickItem & { task: Task } = value as any; + private _isTaskEntry(value: IQuickPickItem): value is IQuickPickItem & { task: Task } { + const candidate: IQuickPickItem & { task: Task } = value as any; return candidate && !!candidate.task; } - private isSettingEntry(value: IQuickPickItem): value is IQuickPickItem & { settingType: string } { - let candidate: IQuickPickItem & { settingType: string } = value as any; + private _isSettingEntry(value: IQuickPickItem): value is IQuickPickItem & { settingType: string } { + const candidate: IQuickPickItem & { settingType: string } = value as any; return candidate && !!candidate.settingType; } - private configureTask(task: Task) { + private _configureTask(task: Task) { if (ContributedTask.is(task)) { this.customize(task, undefined, true); } else if (CustomTask.is(task)) { @@ -3135,21 +3137,21 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private handleSelection(selection: TaskQuickPickEntryType | undefined) { + private _handleSelection(selection: TaskQuickPickEntryType | undefined) { if (!selection) { return; } - if (this.isTaskEntry(selection)) { - this.configureTask(selection.task); - } else if (this.isSettingEntry(selection)) { - const taskQuickPick = new TaskQuickPick(this, this.configurationService, this.quickInputService, this.notificationService, this.dialogService); + if (this._isTaskEntry(selection)) { + this._configureTask(selection.task); + } else if (this._isSettingEntry(selection)) { + const taskQuickPick = new TaskQuickPick(this, this._configurationService, this._quickInputService, this._notificationService, this._themeService, this._dialogService); taskQuickPick.handleSettingOption(selection.settingType); - } else if (selection.folder && (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { - this.openTaskFile(selection.folder.toResource('.vscode/tasks.json'), TaskSourceKind.Workspace); + } else if (selection.folder && (this._contextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { + this._openTaskFile(selection.folder.toResource('.vscode/tasks.json'), TaskSourceKind.Workspace); } else { - const resource = this.getResourceForKind(TaskSourceKind.User); + const resource = this._getResourceForKind(TaskSourceKind.User); if (resource) { - this.openTaskFile(resource, TaskSourceKind.User); + this._openTaskFile(resource, TaskSourceKind.User); } } } @@ -3161,7 +3163,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } else if (task._source.kind === TaskSourceKind.WorkspaceFile) { description = task.getWorkspaceFileName(); } else if (this.needsFolderQualification()) { - let workspaceFolder = task.getWorkspaceFolder(); + const workspaceFolder = task.getWorkspaceFolder(); if (workspaceFolder) { description = workspaceFolder.name; } @@ -3169,38 +3171,40 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return description; } - private async runConfigureTasks(): Promise<void> { - if (!(await this.trust())) { + private async _runConfigureTasks(): Promise<void> { + if (!(await this._trust())) { return; } - if (!this.canRunCommand()) { + if (!this._canRunCommand()) { return undefined; } let taskPromise: Promise<TaskMap>; if (this.schemaVersion === JsonSchemaVersion.V2_0_0) { - taskPromise = this.getGroupedTasks(); + taskPromise = this._getGroupedTasks(); } else { taskPromise = Promise.resolve(new TaskMap()); } - let stats = this.contextService.getWorkspace().folders.map<Promise<IFileStatWithPartialMetadata | undefined>>((folder) => { - return this.fileService.stat(folder.toResource('.vscode/tasks.json')).then(stat => stat, () => undefined); + const stats = this._contextService.getWorkspace().folders.map<Promise<IFileStatWithPartialMetadata | undefined>>((folder) => { + return this._fileService.stat(folder.toResource('.vscode/tasks.json')).then(stat => stat, () => undefined); }); - let createLabel = nls.localize('TaskService.createJsonFile', 'Create tasks.json file from template'); - let openLabel = nls.localize('TaskService.openJsonFile', 'Open tasks.json file'); + const createLabel = nls.localize('TaskService.createJsonFile', 'Create tasks.json file from template'); + const openLabel = nls.localize('TaskService.openJsonFile', 'Open tasks.json file'); const tokenSource = new CancellationTokenSource(); const cancellationToken: CancellationToken = tokenSource.token; - let entries = Promise.all(stats).then((stats) => { + const entries = Promise.all(stats).then((stats) => { return taskPromise.then((taskMap) => { - let entries: QuickPickInput<TaskQuickPickEntryType>[] = []; + const entries: QuickPickInput<TaskQuickPickEntryType>[] = []; let configuredCount = 0; let tasks = taskMap.all(); if (tasks.length > 0) { tasks = tasks.sort((a, b) => a._label.localeCompare(b._label)); - for (let task of tasks) { - entries.push({ label: task._label, task, description: this.getTaskDescription(task), detail: this.showDetail() ? task.configurationProperties.detail : undefined }); + for (const task of tasks) { + const entry = { label: TaskQuickPick.getTaskLabelWithIcon(task), task, description: this.getTaskDescription(task), detail: this._showDetail() ? task.configurationProperties.detail : undefined }; + TaskQuickPick.applyColorStyles(task, entry, this._themeService); + entries.push(entry); if (!ContributedTask.is(task)) { configuredCount++; } @@ -3209,11 +3213,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const needsCreateOrOpen = (configuredCount === 0); // If the only configured tasks are user tasks, then we should also show the option to create from a template. if (needsCreateOrOpen || (taskMap.get(USER_TASKS_GROUP_KEY).length === configuredCount)) { - let label = stats[0] !== undefined ? openLabel : createLabel; + const label = stats[0] !== undefined ? openLabel : createLabel; if (entries.length) { entries.push({ type: 'separator' }); } - entries.push({ label, folder: this.contextService.getWorkspace().folders[0] }); + entries.push({ label, folder: this._contextService.getWorkspace().folders[0] }); } if ((entries.length === 1) && !needsCreateOrOpen) { tokenSource.cancel(); @@ -3231,20 +3235,20 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }, 200); })]); - if (!timeout && ((await entries).length === 1) && this.configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) { + if (!timeout && ((await entries).length === 1) && this._configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) { const entry: any = <any>((await entries)[0]); if (entry.task) { - this.handleSelection(entry); + this._handleSelection(entry); return; } } const entriesWithSettings = entries.then(resolvedEntries => { - resolvedEntries.push(...TaskQuickPick.allSettingEntries(this.configurationService)); + resolvedEntries.push(...TaskQuickPick.allSettingEntries(this._configurationService)); return resolvedEntries; }); - this.quickInputService.pick(entriesWithSettings, + this._quickInputService.pick(entriesWithSettings, { placeHolder: nls.localize('TaskService.pickTask', 'Select a task to configure') }, cancellationToken). then(async (selection) => { if (cancellationToken.isCancellationRequested) { @@ -3254,41 +3258,72 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer selection = <TaskQuickPickEntryType>task; } } - this.handleSelection(selection); + this._handleSelection(selection); }); } - private runConfigureDefaultBuildTask(): void { - if (!this.canRunCommand()) { + private _runConfigureDefaultBuildTask(): void { + if (!this._canRunCommand()) { return; } if (this.schemaVersion === JsonSchemaVersion.V2_0_0) { this.tasks().then((tasks => { if (tasks.length === 0) { - this.runConfigureTasks(); + this._runConfigureTasks(); return; } + const entries: QuickPickInput<TaskQuickPickEntryType>[] = []; let selectedTask: Task | undefined; - let selectedEntry: TaskQuickPickEntry; - for (let task of tasks) { - let taskGroup: TaskGroup | undefined = TaskGroup.from(task.configurationProperties.group); - if (taskGroup && taskGroup.isDefault && taskGroup._id === TaskGroup.Build._id) { - selectedTask = task; - break; + let selectedEntry: TaskQuickPickEntryType | undefined; + this._showIgnoredFoldersMessage().then(() => { + for (const task of tasks) { + const taskGroup: TaskGroup | undefined = TaskGroup.from(task.configurationProperties.group); + if (taskGroup && taskGroup.isDefault && taskGroup._id === TaskGroup.Build._id) { + const label = nls.localize('TaskService.defaultBuildTaskExists', '{0} is already marked as the default build task', TaskQuickPick.getTaskLabelWithIcon(task, task.getQualifiedLabel())); + selectedTask = task; + selectedEntry = { label, task, description: this.getTaskDescription(task), detail: this._showDetail() ? task.configurationProperties.detail : undefined }; + TaskQuickPick.applyColorStyles(task, selectedEntry, this._themeService); + } else { + const entry = { label: TaskQuickPick.getTaskLabelWithIcon(task), task, description: this.getTaskDescription(task), detail: this._showDetail() ? task.configurationProperties.detail : undefined }; + TaskQuickPick.applyColorStyles(task, entry, this._themeService); + entries.push(entry); + } } - } - if (selectedTask) { - selectedEntry = { - label: nls.localize('TaskService.defaultBuildTaskExists', '{0} is already marked as the default build task', selectedTask.getQualifiedLabel()), - task: selectedTask, - detail: this.showDetail() ? selectedTask.configurationProperties.detail : undefined - }; - } - this.showIgnoredFoldersMessage().then(() => { - this.showQuickPick(tasks, - nls.localize('TaskService.pickDefaultBuildTask', 'Select the task to be used as the default build task'), undefined, true, false, selectedEntry). + if (selectedEntry) { + entries.unshift(selectedEntry); + } + const tokenSource = new CancellationTokenSource(); + const cancellationToken: CancellationToken = tokenSource.token; + this._quickInputService.pick(entries, + { placeHolder: nls.localize('TaskService.pickTask', 'Select a task to configure') }, cancellationToken). + then(async (entry) => { + if (cancellationToken.isCancellationRequested) { + // canceled when there's only one task + const task = (await entries)[0]; + if ((<any>task).task) { + entry = <TaskQuickPickEntryType>task; + } + } + const task: Task | undefined | null = entry && 'task' in entry ? entry.task : undefined; + if ((task === undefined) || (task === null)) { + return; + } + if (task === selectedTask && CustomTask.is(task)) { + this.openConfig(task); + } + if (!InMemoryTask.is(task)) { + this.customize(task, { group: { kind: 'build', isDefault: true } }, true).then(() => { + if (selectedTask && (task !== selectedTask) && !InMemoryTask.is(selectedTask)) { + this.customize(selectedTask, { group: 'build' }, false); + } + }); + } + }); + this._quickInputService.pick(entries, { + placeHolder: nls.localize('TaskService.pickDefaultBuildTask', 'Select the task to be used as the default build task') + }). then((entry) => { - let task: Task | undefined | null = entry ? entry.task : undefined; + const task: Task | undefined | null = entry && 'task' in entry ? entry.task : undefined; if ((task === undefined) || (task === null)) { return; } @@ -3306,25 +3341,25 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); })); } else { - this.runConfigureTasks(); + this._runConfigureTasks(); } } - private runConfigureDefaultTestTask(): void { - if (!this.canRunCommand()) { + private _runConfigureDefaultTestTask(): void { + if (!this._canRunCommand()) { return; } if (this.schemaVersion === JsonSchemaVersion.V2_0_0) { this.tasks().then((tasks => { if (tasks.length === 0) { - this.runConfigureTasks(); + this._runConfigureTasks(); return; } let selectedTask: Task | undefined; - let selectedEntry: TaskQuickPickEntry; + let selectedEntry: ITaskQuickPickEntry; - for (let task of tasks) { - let taskGroup: TaskGroup | undefined = TaskGroup.from(task.configurationProperties.group); + for (const task of tasks) { + const taskGroup: TaskGroup | undefined = TaskGroup.from(task.configurationProperties.group); if (taskGroup && taskGroup.isDefault && taskGroup._id === TaskGroup.Test._id) { selectedTask = task; break; @@ -3334,14 +3369,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer selectedEntry = { label: nls.localize('TaskService.defaultTestTaskExists', '{0} is already marked as the default test task.', selectedTask.getQualifiedLabel()), task: selectedTask, - detail: this.showDetail() ? selectedTask.configurationProperties.detail : undefined + detail: this._showDetail() ? selectedTask.configurationProperties.detail : undefined }; } - this.showIgnoredFoldersMessage().then(() => { - this.showQuickPick(tasks, + this._showIgnoredFoldersMessage().then(() => { + this._showQuickPick(tasks, nls.localize('TaskService.pickDefaultTestTask', 'Select the task to be used as the default test task'), undefined, true, false, selectedEntry).then((entry) => { - let task: Task | undefined | null = entry ? entry.task : undefined; + const task: Task | undefined | null = entry ? entry.task : undefined; if (!task) { return; } @@ -3359,12 +3394,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); })); } else { - this.runConfigureTasks(); + this._runConfigureTasks(); } } public async runShowTasks(): Promise<void> { - if (!this.canRunCommand()) { + if (!this._canRunCommand()) { return; } const activeTasksPromise: Promise<Task[]> = this.getActiveTasks(); @@ -3384,7 +3419,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer })) { this._taskSystem!.revealTask(activeTasks[0]); } else { - this.showQuickPick(activeTasksPromise, + this._showQuickPick(activeTasksPromise, nls.localize('TaskService.pickShowTask', 'Select the task to show its output'), { label: nls.localize('TaskService.noTaskIsRunning', 'No task is running'), @@ -3392,7 +3427,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }, false, true ).then((entry) => { - let task: Task | undefined | null = entry ? entry.task : undefined; + const task: Task | undefined | null = entry ? entry.task : undefined; if (task === undefined || task === null) { return; } @@ -3401,17 +3436,17 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } } - private async createTasksDotOld(folder: IWorkspaceFolder): Promise<[URI, URI] | undefined> { + private async _createTasksDotOld(folder: IWorkspaceFolder): Promise<[URI, URI] | undefined> { const tasksFile = folder.toResource('.vscode/tasks.json'); - if (await this.fileService.exists(tasksFile)) { + if (await this._fileService.exists(tasksFile)) { const oldFile = tasksFile.with({ path: `${tasksFile.path}.old` }); - await this.fileService.copy(tasksFile, oldFile, true); + await this._fileService.copy(tasksFile, oldFile, true); return [oldFile, tasksFile]; } return undefined; } - private upgradeTask(task: Task, suppressTaskName: boolean, globalConfig: { windows?: CommandUpgrade; osx?: CommandUpgrade; linux?: CommandUpgrade }): TaskConfig.CustomTask | TaskConfig.ConfiguringTask | undefined { + private _upgradeTask(task: Task, suppressTaskName: boolean, globalConfig: { windows?: ICommandUpgrade; osx?: ICommandUpgrade; linux?: ICommandUpgrade }): TaskConfig.ICustomTask | TaskConfig.IConfiguringTask | undefined { if (!CustomTask.is(task)) { return; } @@ -3455,31 +3490,31 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer task._source.config.element = configElement; const tempTask = new CustomTask(task._id, task._source, task._label, task.type, task.command, task.hasDefinedMatchers, task.runOptions, task.configurationProperties); - const configTask = this.createCustomizableTask(tempTask); + const configTask = this._createCustomizableTask(tempTask); if (configTask) { return configTask; } return; } - private async upgrade(): Promise<void> { + private async _upgrade(): Promise<void> { if (this.schemaVersion === JsonSchemaVersion.V2_0_0) { return; } - if (!this.workspaceTrustManagementService.isWorkspaceTrusted()) { - this._register(Event.once(this.workspaceTrustManagementService.onDidChangeTrust)(isTrusted => { + if (!this._workspaceTrustManagementService.isWorkspaceTrusted()) { + this._register(Event.once(this._workspaceTrustManagementService.onDidChangeTrust)(isTrusted => { if (isTrusted) { - this.upgrade(); + this._upgrade(); } })); return; } - const tasks = await this.getGroupedTasks(); + const tasks = await this._getGroupedTasks(); const fileDiffs: [URI, URI][] = []; for (const folder of this.workspaceFolders) { - const diff = await this.createTasksDotOld(folder); + const diff = await this._createTasksDotOld(folder); if (diff) { fileDiffs.push(diff); } @@ -3487,36 +3522,36 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer continue; } - const configTasks: (TaskConfig.CustomTask | TaskConfig.ConfiguringTask)[] = []; - const suppressTaskName = !!this.configurationService.getValue('tasks.suppressTaskName', { resource: folder.uri }); + const configTasks: (TaskConfig.ICustomTask | TaskConfig.IConfiguringTask)[] = []; + const suppressTaskName = !!this._configurationService.getValue('tasks.suppressTaskName', { resource: folder.uri }); const globalConfig = { - windows: <CommandUpgrade>this.configurationService.getValue('tasks.windows', { resource: folder.uri }), - osx: <CommandUpgrade>this.configurationService.getValue('tasks.osx', { resource: folder.uri }), - linux: <CommandUpgrade>this.configurationService.getValue('tasks.linux', { resource: folder.uri }) + windows: <ICommandUpgrade>this._configurationService.getValue('tasks.windows', { resource: folder.uri }), + osx: <ICommandUpgrade>this._configurationService.getValue('tasks.osx', { resource: folder.uri }), + linux: <ICommandUpgrade>this._configurationService.getValue('tasks.linux', { resource: folder.uri }) }; tasks.get(folder).forEach(task => { - const configTask = this.upgradeTask(task, suppressTaskName, globalConfig); + const configTask = this._upgradeTask(task, suppressTaskName, globalConfig); if (configTask) { configTasks.push(configTask); } }); this._taskSystem = undefined; this._workspaceTasksPromise = undefined; - await this.writeConfiguration(folder, 'tasks.tasks', configTasks); - await this.writeConfiguration(folder, 'tasks.version', '2.0.0'); - if (this.configurationService.getValue('tasks.showOutput', { resource: folder.uri })) { - await this.configurationService.updateValue('tasks.showOutput', undefined, { resource: folder.uri }); + await this._writeConfiguration(folder, 'tasks.tasks', configTasks); + await this._writeConfiguration(folder, 'tasks.version', '2.0.0'); + if (this._configurationService.getValue('tasks.showOutput', { resource: folder.uri })) { + await this._configurationService.updateValue('tasks.showOutput', undefined, { resource: folder.uri }); } - if (this.configurationService.getValue('tasks.isShellCommand', { resource: folder.uri })) { - await this.configurationService.updateValue('tasks.isShellCommand', undefined, { resource: folder.uri }); + if (this._configurationService.getValue('tasks.isShellCommand', { resource: folder.uri })) { + await this._configurationService.updateValue('tasks.isShellCommand', undefined, { resource: folder.uri }); } - if (this.configurationService.getValue('tasks.suppressTaskName', { resource: folder.uri })) { - await this.configurationService.updateValue('tasks.suppressTaskName', undefined, { resource: folder.uri }); + if (this._configurationService.getValue('tasks.suppressTaskName', { resource: folder.uri })) { + await this._configurationService.updateValue('tasks.suppressTaskName', undefined, { resource: folder.uri }); } } - this.updateSetup(); + this._updateSetup(); - this.notificationService.prompt(Severity.Warning, + this._notificationService.prompt(Severity.Warning, fileDiffs.length === 1 ? nls.localize('taskService.upgradeVersion', "The deprecated tasks version 0.1.0 has been removed. Your tasks have been upgraded to version 2.0.0. Open the diff to review the upgrade.") : nls.localize('taskService.upgradeVersionPlural', "The deprecated tasks version 0.1.0 has been removed. Your tasks have been upgraded to version 2.0.0. Open the diffs to review the upgrade."), @@ -3524,7 +3559,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer label: fileDiffs.length === 1 ? nls.localize('taskService.openDiff', "Open diff") : nls.localize('taskService.openDiffs', "Open diffs"), run: async () => { for (const upgrade of fileDiffs) { - await this.editorService.openEditor({ + await this._editorService.openEditor({ original: { resource: upgrade[0] }, modified: { resource: upgrade[1] } }); diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 270dafb67c2..3d91091af90 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -7,9 +7,9 @@ import * as nls from 'vs/nls'; import * as resources from 'vs/base/common/resources'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { ITaskService, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; +import { ITaskService, IWorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; import { forEach } from 'vs/base/common/collections'; -import { RunOnOptions, Task, TaskRunSource, TaskSource, TaskSourceKind, TASKS_CATEGORY, WorkspaceFileTaskSource, WorkspaceTaskSource } from 'vs/workbench/contrib/tasks/common/tasks'; +import { RunOnOptions, Task, TaskRunSource, TaskSource, TaskSourceKind, TASKS_CATEGORY, WorkspaceFileTaskSource, IWorkspaceTaskSource } from 'vs/workbench/contrib/tasks/common/tasks'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -26,40 +26,40 @@ const ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE = 'tasks.run.allowAutomatic'; export class RunAutomaticTasks extends Disposable implements IWorkbenchContribution { constructor( - @ITaskService private readonly taskService: ITaskService, - @IStorageService private readonly storageService: IStorageService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @ILogService private readonly logService: ILogService) { + @ITaskService private readonly _taskService: ITaskService, + @IStorageService private readonly _storageService: IStorageService, + @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, + @ILogService private readonly _logService: ILogService) { super(); - this.tryRunTasks(); + this._tryRunTasks(); } - private async tryRunTasks() { - this.logService.trace('RunAutomaticTasks: Trying to run tasks.'); + private async _tryRunTasks() { + this._logService.trace('RunAutomaticTasks: Trying to run tasks.'); // Wait until we have task system info (the extension host and workspace folders are available). - if (!this.taskService.hasTaskSystemInfo) { - this.logService.trace('RunAutomaticTasks: Awaiting task system info.'); - await Event.toPromise(Event.once(this.taskService.onDidChangeTaskSystemInfo)); + if (!this._taskService.hasTaskSystemInfo) { + this._logService.trace('RunAutomaticTasks: Awaiting task system info.'); + await Event.toPromise(Event.once(this._taskService.onDidChangeTaskSystemInfo)); } - this.logService.trace('RunAutomaticTasks: Checking if automatic tasks should run.'); - const isFolderAutomaticAllowed = this.storageService.getBoolean(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, StorageScope.WORKSPACE, undefined); - await this.workspaceTrustManagementService.workspaceTrustInitialized; - const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted(); + this._logService.trace('RunAutomaticTasks: Checking if automatic tasks should run.'); + const isFolderAutomaticAllowed = this._storageService.getBoolean(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, StorageScope.WORKSPACE, undefined); + await this._workspaceTrustManagementService.workspaceTrustInitialized; + const isWorkspaceTrusted = this._workspaceTrustManagementService.isWorkspaceTrusted(); // Only run if allowed. Prompting for permission occurs when a user first tries to run a task. if (isFolderAutomaticAllowed && isWorkspaceTrusted) { - this.taskService.getWorkspaceTasks(TaskRunSource.FolderOpen).then(workspaceTaskResult => { - let { tasks } = RunAutomaticTasks.findAutoTasks(this.taskService, workspaceTaskResult); - this.logService.trace(`RunAutomaticTasks: Found ${tasks.length} automatic tasks tasks`); + this._taskService.getWorkspaceTasks(TaskRunSource.FolderOpen).then(workspaceTaskResult => { + const { tasks } = RunAutomaticTasks._findAutoTasks(this._taskService, workspaceTaskResult); + this._logService.trace(`RunAutomaticTasks: Found ${tasks.length} automatic tasks tasks`); if (tasks.length > 0) { - RunAutomaticTasks.runTasks(this.taskService, tasks); + RunAutomaticTasks._runTasks(this._taskService, tasks); } }); } } - private static runTasks(taskService: ITaskService, tasks: Array<Task | Promise<Task | undefined>>) { + private static _runTasks(taskService: ITaskService, tasks: Array<Task | Promise<Task | undefined>>) { tasks.forEach(task => { if (task instanceof Promise) { task.then(promiseResult => { @@ -73,11 +73,11 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut }); } - private static getTaskSource(source: TaskSource): URI | undefined { + private static _getTaskSource(source: TaskSource): URI | undefined { const taskKind = TaskSourceKind.toConfigurationTarget(source.kind); switch (taskKind) { case ConfigurationTarget.WORKSPACE_FOLDER: { - return resources.joinPath((<WorkspaceTaskSource>source).config.workspaceFolder!.uri, (<WorkspaceTaskSource>source).config.file); + return resources.joinPath((<IWorkspaceTaskSource>source).config.workspaceFolder!.uri, (<IWorkspaceTaskSource>source).config.file); } case ConfigurationTarget.WORKSPACE: { return (<WorkspaceFileTaskSource>source).config.workspace?.configuration ?? undefined; @@ -86,7 +86,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut return undefined; } - private static findAutoTasks(taskService: ITaskService, workspaceTaskResult: Map<string, WorkspaceFolderTaskResult>): { tasks: Array<Task | Promise<Task | undefined>>; taskNames: Array<string>; locations: Map<string, URI> } { + private static _findAutoTasks(taskService: ITaskService, workspaceTaskResult: Map<string, IWorkspaceFolderTaskResult>): { tasks: Array<Task | Promise<Task | undefined>>; taskNames: Array<string>; locations: Map<string, URI> } { const tasks = new Array<Task | Promise<Task | undefined>>(); const taskNames = new Array<string>(); const locations = new Map<string, URI>(); @@ -98,7 +98,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut if (task.runOptions.runOn === RunOnOptions.folderOpen) { tasks.push(task); taskNames.push(task._label); - const location = RunAutomaticTasks.getTaskSource(task._source); + const location = RunAutomaticTasks._getTaskSource(task._source); if (location) { locations.set(location.fsPath, location); } @@ -116,7 +116,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut } else { taskNames.push(configedTask.value.configures.task); } - const location = RunAutomaticTasks.getTaskSource(configedTask.value._source); + const location = RunAutomaticTasks._getTaskSource(configedTask.value._source); if (location) { locations.set(location.fsPath, location); } @@ -129,7 +129,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut } public static async promptForPermission(taskService: ITaskService, storageService: IStorageService, notificationService: INotificationService, workspaceTrustManagementService: IWorkspaceTrustManagementService, - openerService: IOpenerService, workspaceTaskResult: Map<string, WorkspaceFolderTaskResult>) { + openerService: IOpenerService, workspaceTaskResult: Map<string, IWorkspaceFolderTaskResult>) { const isWorkspaceTrusted = workspaceTrustManagementService.isWorkspaceTrusted; if (!isWorkspaceTrusted) { return; @@ -140,18 +140,18 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut return; } - let { tasks, taskNames, locations } = RunAutomaticTasks.findAutoTasks(taskService, workspaceTaskResult); + const { tasks, taskNames, locations } = RunAutomaticTasks._findAutoTasks(taskService, workspaceTaskResult); if (taskNames.length > 0) { // We have automatic tasks, prompt to allow. - this.showPrompt(notificationService, storageService, taskService, openerService, taskNames, locations).then(allow => { + this._showPrompt(notificationService, storageService, taskService, openerService, taskNames, locations).then(allow => { if (allow) { - RunAutomaticTasks.runTasks(taskService, tasks); + RunAutomaticTasks._runTasks(taskService, tasks); } }); } } - private static showPrompt(notificationService: INotificationService, storageService: IStorageService, taskService: ITaskService, + private static _showPrompt(notificationService: INotificationService, storageService: IStorageService, taskService: ITaskService, openerService: IOpenerService, taskNames: Array<string>, locations: Map<string, URI>): Promise<boolean> { return new Promise<boolean>(resolve => { notificationService.prompt(Severity.Info, nls.localize('tasks.run.allowAutomatic', diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 4b5b0871acb..eff66ddba5f 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -20,7 +20,7 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatus import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/services/output/common/output'; -import { TaskEvent, TaskEventKind, TaskGroup, TASKS_CATEGORY, TASK_RUNNING_STATE } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskEvent, TaskEventKind, TaskGroup, TASKS_CATEGORY, TASK_RUNNING_STATE } from 'vs/workbench/contrib/tasks/common/tasks'; import { ITaskService, ProcessExecutionSupportedContext, ShellExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -56,31 +56,31 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { }); export class TaskStatusBarContributions extends Disposable implements IWorkbenchContribution { - private runningTasksStatusItem: IStatusbarEntryAccessor | undefined; - private activeTasksCount: number = 0; + private _runningTasksStatusItem: IStatusbarEntryAccessor | undefined; + private _activeTasksCount: number = 0; constructor( - @ITaskService private readonly taskService: ITaskService, - @IStatusbarService private readonly statusbarService: IStatusbarService, - @IProgressService private readonly progressService: IProgressService + @ITaskService private readonly _taskService: ITaskService, + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @IProgressService private readonly _progressService: IProgressService ) { super(); - this.registerListeners(); + this._registerListeners(); } - private registerListeners(): void { + private _registerListeners(): void { let promise: Promise<void> | undefined = undefined; let resolver: (value?: void | Thenable<void>) => void; - this.taskService.onDidStateChange(event => { + this._taskService.onDidStateChange(event => { if (event.kind === TaskEventKind.Changed) { - this.updateRunningTasksStatus(); + this._updateRunningTasksStatus(); } - if (!this.ignoreEventForUpdateRunningTasksCount(event)) { + if (!this._ignoreEventForUpdateRunningTasksCount(event)) { switch (event.kind) { case TaskEventKind.Active: - this.activeTasksCount++; - if (this.activeTasksCount === 1) { + this._activeTasksCount++; + if (this._activeTasksCount === 1) { if (!promise) { promise = new Promise<void>((resolve) => { resolver = resolve; @@ -91,9 +91,9 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench case TaskEventKind.Inactive: // Since the exiting of the sub process is communicated async we can't order inactive and terminate events. // So try to treat them accordingly. - if (this.activeTasksCount > 0) { - this.activeTasksCount--; - if (this.activeTasksCount === 0) { + if (this._activeTasksCount > 0) { + this._activeTasksCount--; + if (this._activeTasksCount === 0) { if (promise && resolver!) { resolver!(); } @@ -101,8 +101,8 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench } break; case TaskEventKind.Terminated: - if (this.activeTasksCount !== 0) { - this.activeTasksCount = 0; + if (this._activeTasksCount !== 0) { + this._activeTasksCount = 0; if (promise && resolver!) { resolver!(); } @@ -111,8 +111,8 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench } } - if (promise && (event.kind === TaskEventKind.Active) && (this.activeTasksCount === 1)) { - this.progressService.withProgress({ location: ProgressLocation.Window, command: 'workbench.action.tasks.showTasks' }, progress => { + if (promise && (event.kind === TaskEventKind.Active) && (this._activeTasksCount === 1)) { + this._progressService.withProgress({ location: ProgressLocation.Window, command: 'workbench.action.tasks.showTasks' }, progress => { progress.report({ message: nls.localize('building', 'Building...') }); return promise!; }).then(() => { @@ -122,12 +122,12 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench }); } - private async updateRunningTasksStatus(): Promise<void> { - const tasks = await this.taskService.getActiveTasks(); + private async _updateRunningTasksStatus(): Promise<void> { + const tasks = await this._taskService.getActiveTasks(); if (tasks.length === 0) { - if (this.runningTasksStatusItem) { - this.runningTasksStatusItem.dispose(); - this.runningTasksStatusItem = undefined; + if (this._runningTasksStatusItem) { + this._runningTasksStatusItem.dispose(); + this._runningTasksStatusItem = undefined; } } else { const itemProps: IStatusbarEntry = { @@ -138,16 +138,16 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench command: 'workbench.action.tasks.showTasks', }; - if (!this.runningTasksStatusItem) { - this.runningTasksStatusItem = this.statusbarService.addEntry(itemProps, 'status.runningTasks', StatusbarAlignment.LEFT, 49 /* Medium Priority, next to Markers */); + if (!this._runningTasksStatusItem) { + this._runningTasksStatusItem = this._statusbarService.addEntry(itemProps, 'status.runningTasks', StatusbarAlignment.LEFT, 49 /* Medium Priority, next to Markers */); } else { - this.runningTasksStatusItem.update(itemProps); + this._runningTasksStatusItem.update(itemProps); } } } - private ignoreEventForUpdateRunningTasksCount(event: TaskEvent): boolean { - if (!this.taskService.inTerminal()) { + private _ignoreEventForUpdateRunningTasksCount(event: ITaskEvent): boolean { + if (!this._taskService.inTerminal()) { return false; } @@ -364,7 +364,7 @@ KeybindingsRegistry.registerKeybindingRule({ }); // Tasks Output channel. Register it before using it in Task Service. -let outputChannelRegistry = Registry.as<IOutputChannelRegistry>(OutputExt.OutputChannels); +const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(OutputExt.OutputChannels); outputChannelRegistry.registerChannel({ id: AbstractTaskService.OutputChannelId, label: AbstractTaskService.OutputChannelLabel, log: false }); @@ -377,11 +377,11 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: TasksQuickAccessProvider.PREFIX, contextKey: tasksPickerContextKey, placeholder: nls.localize('tasksQuickAccessPlaceholder', "Type the name of a task to run."), - helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Run Task"), needsEditor: false }] + helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Run Task") }] }); // tasks.json validation -let schema: IJSONSchema = { +const schema: IJSONSchema = { id: tasksSchemaId, description: 'Task definition file', type: 'object', @@ -411,7 +411,7 @@ schema.definitions = { }; schema.oneOf = [...(schemaVersion2.oneOf || []), ...(schemaVersion1.oneOf || [])]; -let jsonRegistry = <jsonContributionRegistry.IJSONContributionRegistry>Registry.as(jsonContributionRegistry.Extensions.JSONContribution); +const jsonRegistry = <jsonContributionRegistry.IJSONContributionRegistry>Registry.as(jsonContributionRegistry.Extensions.JSONContribution); jsonRegistry.registerSchema(tasksSchemaId, schema); ProblemMatcherRegistry.onMatcherChanged(() => { @@ -496,6 +496,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('task.quickOpen.showAll', "Causes the Tasks: Run Task command to use the slower \"show all\" behavior instead of the faster two level picker where tasks are grouped by provider."), default: false }, + 'task.showDecorations': { + type: 'boolean', + description: nls.localize('task.showDecorations', "Shows decorations at points of interest in the terminal buffer such as the first problem found via a watch task. Note that this will only take effect for future tasks."), + default: true + }, 'task.saveBeforeRun': { markdownDescription: nls.localize( 'task.saveBeforeRun', diff --git a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts index c78e28e9d0c..77dc6f60769 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskQuickPick.ts @@ -8,7 +8,7 @@ import * as Objects from 'vs/base/common/objects'; import { Task, ContributedTask, CustomTask, ConfiguringTask, TaskSorter, KeyedTaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import * as Types from 'vs/base/common/types'; -import { ITaskService, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; +import { ITaskService, IWorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; import { IQuickPickItem, QuickPickInput, IQuickPick, IQuickInputButton } from 'vs/base/parts/quickinput/common/quickInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -16,9 +16,11 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Codicon } from 'vs/base/common/codicons'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { getColorClass, getColorStyleElement } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; +import { TaskQuickPickEntryType } from 'vs/workbench/contrib/tasks/browser/abstractTaskService'; export const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail'; export const QUICKOPEN_SKIP_CONFIG = 'task.quickOpen.skip'; @@ -27,11 +29,11 @@ export function isWorkspaceFolder(folder: IWorkspace | IWorkspaceFolder): folder return 'uri' in folder; } -export interface TaskQuickPickEntry extends IQuickPickItem { +export interface ITaskQuickPickEntry extends IQuickPickItem { task: Task | undefined | null; } -export interface TaskTwoLevelQuickPickEntry extends IQuickPickItem { +export interface ITaskTwoLevelQuickPickEntry extends IQuickPickItem { task: Task | ConfiguringTask | string | undefined | null; settingType?: string; } @@ -42,24 +44,25 @@ export const configureTaskIcon = registerIcon('tasks-list-configure', Codicon.ge const removeTaskIcon = registerIcon('tasks-remove', Codicon.close, nls.localize('removeTaskIcon', 'Icon for remove in the tasks selection list.')); export class TaskQuickPick extends Disposable { - private sorter: TaskSorter; - private topLevelEntries: QuickPickInput<TaskTwoLevelQuickPickEntry>[] | undefined; + private _sorter: TaskSorter; + private _topLevelEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[] | undefined; constructor( - private taskService: ITaskService, - private configurationService: IConfigurationService, - private quickInputService: IQuickInputService, - private notificationService: INotificationService, - private dialogService: IDialogService) { + private _taskService: ITaskService, + private _configurationService: IConfigurationService, + private _quickInputService: IQuickInputService, + private _notificationService: INotificationService, + private _themeService: IThemeService, + private _dialogService: IDialogService) { super(); - this.sorter = this.taskService.createSorter(); + this._sorter = this._taskService.createSorter(); } - private showDetail(): boolean { + private _showDetail(): boolean { // Ensure invalid values get converted into boolean values - return !!this.configurationService.getValue(QUICKOPEN_DETAIL_CONFIG); + return !!this._configurationService.getValue(QUICKOPEN_DETAIL_CONFIG); } - private guessTaskLabel(task: Task | ConfiguringTask): string { + private _guessTaskLabel(task: Task | ConfiguringTask): string { if (task._label) { return task._label; } @@ -74,21 +77,42 @@ export class TaskQuickPick extends Disposable { return ''; } - private createTaskEntry(task: Task | ConfiguringTask, extraButtons: IQuickInputButton[] = []): TaskTwoLevelQuickPickEntry { - const entry: TaskTwoLevelQuickPickEntry = { label: this.guessTaskLabel(task), description: this.taskService.getTaskDescription(task), task, detail: this.showDetail() ? task.configurationProperties.detail : undefined }; - entry.buttons = [{ iconClass: ThemeIcon.asClassName(configureTaskIcon), tooltip: nls.localize('configureTask', "Configure Task") }, ...extraButtons]; + public static getTaskLabelWithIcon(task: Task | ConfiguringTask, labelGuess?: string): string { + const label = labelGuess || task._label; + const icon = task.configurationProperties.icon; + if (!icon) { + return `${label}`; + } + return icon.id ? `$(${icon.id}) ${label}` : `$(${Codicon.tools.id}) ${label}`; + } + + public static applyColorStyles(task: Task | ConfiguringTask, entry: TaskQuickPickEntryType | ITaskTwoLevelQuickPickEntry, themeService: IThemeService): void { + if (task.configurationProperties.icon?.color) { + const colorTheme = themeService.getColorTheme(); + const styleElement = getColorStyleElement(colorTheme); + entry.iconClasses = [getColorClass(task.configurationProperties.icon.color)]; + document.body.appendChild(styleElement); + } + } + + private _createTaskEntry(task: Task | ConfiguringTask, extraButtons: IQuickInputButton[] = []): ITaskTwoLevelQuickPickEntry { + const entry: ITaskTwoLevelQuickPickEntry = { label: TaskQuickPick.getTaskLabelWithIcon(task, this._guessTaskLabel(task)), description: this._taskService.getTaskDescription(task), task, detail: this._showDetail() ? task.configurationProperties.detail : undefined }; + entry.buttons = []; + entry.buttons.push({ iconClass: ThemeIcon.asClassName(configureTaskIcon), tooltip: nls.localize('configureTask', "Configure Task") }); + entry.buttons.push(...extraButtons); + TaskQuickPick.applyColorStyles(task, entry, this._themeService); return entry; } - private createEntriesForGroup(entries: QuickPickInput<TaskTwoLevelQuickPickEntry>[], tasks: (Task | ConfiguringTask)[], + private _createEntriesForGroup(entries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[], tasks: (Task | ConfiguringTask)[], groupLabel: string, extraButtons: IQuickInputButton[] = []) { entries.push({ type: 'separator', label: groupLabel }); tasks.forEach(task => { - entries.push(this.createTaskEntry(task, extraButtons)); + entries.push(this._createTaskEntry(task, extraButtons)); }); } - private createTypeEntries(entries: QuickPickInput<TaskTwoLevelQuickPickEntry>[], types: string[]) { + private _createTypeEntries(entries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[], types: string[]) { entries.push({ type: 'separator', label: nls.localize('contributedTasks', "contributed") }); types.forEach(type => { entries.push({ label: `$(folder) ${type}`, task: type, ariaLabel: nls.localize('taskType', "All {0} tasks", type) }); @@ -96,8 +120,8 @@ export class TaskQuickPick extends Disposable { entries.push({ label: SHOW_ALL, task: SHOW_ALL, alwaysShow: true }); } - private handleFolderTaskResult(result: Map<string, WorkspaceFolderTaskResult>): (Task | ConfiguringTask)[] { - let tasks: (Task | ConfiguringTask)[] = []; + private _handleFolderTaskResult(result: Map<string, IWorkspaceFolderTaskResult>): (Task | ConfiguringTask)[] { + const tasks: (Task | ConfiguringTask)[] = []; Array.from(result).forEach(([key, folderTasks]) => { if (folderTasks.set) { tasks.push(...folderTasks.set.tasks); @@ -111,7 +135,7 @@ export class TaskQuickPick extends Disposable { return tasks; } - private dedupeConfiguredAndRecent(recentTasks: (Task | ConfiguringTask)[], configuredTasks: (Task | ConfiguringTask)[]): { configuredTasks: (Task | ConfiguringTask)[]; recentTasks: (Task | ConfiguringTask)[] } { + private _dedupeConfiguredAndRecent(recentTasks: (Task | ConfiguringTask)[], configuredTasks: (Task | ConfiguringTask)[]): { configuredTasks: (Task | ConfiguringTask)[]; recentTasks: (Task | ConfiguringTask)[] } { let dedupedConfiguredTasks: (Task | ConfiguringTask)[] = []; const foundRecentTasks: boolean[] = Array(recentTasks.length).fill(false); for (let j = 0; j < configuredTasks.length; j++) { @@ -132,7 +156,7 @@ export class TaskQuickPick extends Disposable { foundRecentTasks[findIndex] = true; } } - dedupedConfiguredTasks = dedupedConfiguredTasks.sort((a, b) => this.sorter.compare(a, b)); + dedupedConfiguredTasks = dedupedConfiguredTasks.sort((a, b) => this._sorter.compare(a, b)); const prunedRecentTasks: (Task | ConfiguringTask)[] = []; for (let i = 0; i < recentTasks.length; i++) { if (foundRecentTasks[i] || ConfiguringTask.is(recentTasks[i])) { @@ -142,88 +166,88 @@ export class TaskQuickPick extends Disposable { return { configuredTasks: dedupedConfiguredTasks, recentTasks: prunedRecentTasks }; } - public async getTopLevelEntries(defaultEntry?: TaskQuickPickEntry): Promise<{ entries: QuickPickInput<TaskTwoLevelQuickPickEntry>[]; isSingleConfigured?: Task | ConfiguringTask }> { - if (this.topLevelEntries !== undefined) { - return { entries: this.topLevelEntries }; + public async getTopLevelEntries(defaultEntry?: ITaskQuickPickEntry): Promise<{ entries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[]; isSingleConfigured?: Task | ConfiguringTask }> { + if (this._topLevelEntries !== undefined) { + return { entries: this._topLevelEntries }; } - let recentTasks: (Task | ConfiguringTask)[] = (await this.taskService.readRecentTasks()).reverse(); - const configuredTasks: (Task | ConfiguringTask)[] = this.handleFolderTaskResult(await this.taskService.getWorkspaceTasks()); - const extensionTaskTypes = this.taskService.taskTypes(); - this.topLevelEntries = []; + let recentTasks: (Task | ConfiguringTask)[] = (await this._taskService.readRecentTasks()).reverse(); + const configuredTasks: (Task | ConfiguringTask)[] = this._handleFolderTaskResult(await this._taskService.getWorkspaceTasks()); + const extensionTaskTypes = this._taskService.taskTypes(); + this._topLevelEntries = []; // Dedupe will update recent tasks if they've changed in tasks.json. - const dedupeAndPrune = this.dedupeConfiguredAndRecent(recentTasks, configuredTasks); - let dedupedConfiguredTasks: (Task | ConfiguringTask)[] = dedupeAndPrune.configuredTasks; + const dedupeAndPrune = this._dedupeConfiguredAndRecent(recentTasks, configuredTasks); + const dedupedConfiguredTasks: (Task | ConfiguringTask)[] = dedupeAndPrune.configuredTasks; recentTasks = dedupeAndPrune.recentTasks; if (recentTasks.length > 0) { const removeRecentButton: IQuickInputButton = { iconClass: ThemeIcon.asClassName(removeTaskIcon), tooltip: nls.localize('removeRecent', 'Remove Recently Used Task') }; - this.createEntriesForGroup(this.topLevelEntries, recentTasks, nls.localize('recentlyUsed', 'recently used'), [removeRecentButton]); + this._createEntriesForGroup(this._topLevelEntries, recentTasks, nls.localize('recentlyUsed', 'recently used'), [removeRecentButton]); } if (configuredTasks.length > 0) { if (dedupedConfiguredTasks.length > 0) { - this.createEntriesForGroup(this.topLevelEntries, dedupedConfiguredTasks, nls.localize('configured', 'configured')); + this._createEntriesForGroup(this._topLevelEntries, dedupedConfiguredTasks, nls.localize('configured', 'configured')); } } if (defaultEntry && (configuredTasks.length === 0)) { - this.topLevelEntries.push({ type: 'separator', label: nls.localize('configured', 'configured') }); - this.topLevelEntries.push(defaultEntry); + this._topLevelEntries.push({ type: 'separator', label: nls.localize('configured', 'configured') }); + this._topLevelEntries.push(defaultEntry); } if (extensionTaskTypes.length > 0) { - this.createTypeEntries(this.topLevelEntries, extensionTaskTypes); + this._createTypeEntries(this._topLevelEntries, extensionTaskTypes); } - return { entries: this.topLevelEntries, isSingleConfigured: configuredTasks.length === 1 ? configuredTasks[0] : undefined }; + return { entries: this._topLevelEntries, isSingleConfigured: configuredTasks.length === 1 ? configuredTasks[0] : undefined }; } public async handleSettingOption(selectedType: string) { const noButton = nls.localize('TaskQuickPick.changeSettingNo', "No"); const yesButton = nls.localize('TaskQuickPick.changeSettingYes', "Yes"); - const changeSettingResult = await this.dialogService.show(Severity.Warning, + const changeSettingResult = await this._dialogService.show(Severity.Warning, nls.localize('TaskQuickPick.changeSettingDetails', "Task detection for {0} tasks causes files in any workspace you open to be run as code. Enabling {0} task detection is a user setting and will apply to any workspace you open. Do you want to enable {0} task detection for all workspaces?", selectedType), [noButton, yesButton]); if (changeSettingResult.choice === 1) { - await this.configurationService.updateValue(`${selectedType}.autoDetect`, 'on'); + await this._configurationService.updateValue(`${selectedType}.autoDetect`, 'on'); await new Promise<void>(resolve => setTimeout(() => resolve(), 100)); return this.show(nls.localize('TaskService.pickRunTask', 'Select the task to run'), undefined, selectedType); } return undefined; } - public async show(placeHolder: string, defaultEntry?: TaskQuickPickEntry, startAtType?: string): Promise<Task | undefined | null> { - const picker: IQuickPick<TaskTwoLevelQuickPickEntry> = this.quickInputService.createQuickPick(); + public async show(placeHolder: string, defaultEntry?: ITaskQuickPickEntry, startAtType?: string): Promise<Task | undefined | null> { + const picker: IQuickPick<ITaskTwoLevelQuickPickEntry> = this._quickInputService.createQuickPick(); picker.placeholder = placeHolder; picker.matchOnDescription = true; picker.ignoreFocusOut = false; picker.show(); picker.onDidTriggerItemButton(async (context) => { - let task = context.item.task; + const task = context.item.task; if (context.button.iconClass === ThemeIcon.asClassName(removeTaskIcon)) { const key = (task && !Types.isString(task)) ? task.getRecentlyUsedKey() : undefined; if (key) { - this.taskService.removeRecentlyUsedTask(key); + this._taskService.removeRecentlyUsedTask(key); } const indexToRemove = picker.items.indexOf(context.item); if (indexToRemove >= 0) { picker.items = [...picker.items.slice(0, indexToRemove), ...picker.items.slice(indexToRemove + 1)]; } } else { - this.quickInputService.cancel(); + this._quickInputService.cancel(); if (ContributedTask.is(task)) { - this.taskService.customize(task, undefined, true); + this._taskService.customize(task, undefined, true); } else if (CustomTask.is(task) || ConfiguringTask.is(task)) { let canOpenConfig: boolean = false; try { - canOpenConfig = await this.taskService.openConfig(task); + canOpenConfig = await this._taskService.openConfig(task); } catch (e) { // do nothing. } if (!canOpenConfig) { - this.taskService.customize(task, undefined, true); + this._taskService.customize(task, undefined, true); } } } @@ -233,30 +257,30 @@ export class TaskQuickPick extends Disposable { if (!firstLevelTask) { // First show recent tasks configured tasks. Other tasks will be available at a second level const topLevelEntriesResult = await this.getTopLevelEntries(defaultEntry); - if (topLevelEntriesResult.isSingleConfigured && this.configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) { + if (topLevelEntriesResult.isSingleConfigured && this._configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) { picker.dispose(); - return this.toTask(topLevelEntriesResult.isSingleConfigured); + return this._toTask(topLevelEntriesResult.isSingleConfigured); } - const taskQuickPickEntries: QuickPickInput<TaskTwoLevelQuickPickEntry>[] = topLevelEntriesResult.entries; - firstLevelTask = await this.doPickerFirstLevel(picker, taskQuickPickEntries); + const taskQuickPickEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[] = topLevelEntriesResult.entries; + firstLevelTask = await this._doPickerFirstLevel(picker, taskQuickPickEntries); } do { if (Types.isString(firstLevelTask)) { // Proceed to second level of quick pick - const selectedEntry = await this.doPickerSecondLevel(picker, firstLevelTask); + const selectedEntry = await this._doPickerSecondLevel(picker, firstLevelTask); if (selectedEntry && !selectedEntry.settingType && selectedEntry.task === null) { // The user has chosen to go back to the first level - firstLevelTask = await this.doPickerFirstLevel(picker, (await this.getTopLevelEntries(defaultEntry)).entries); + firstLevelTask = await this._doPickerFirstLevel(picker, (await this.getTopLevelEntries(defaultEntry)).entries); } else if (selectedEntry && Types.isString(selectedEntry.settingType)) { picker.dispose(); return this.handleSettingOption(selectedEntry.settingType); } else { picker.dispose(); - return (selectedEntry?.task && !Types.isString(selectedEntry?.task)) ? this.toTask(selectedEntry?.task) : undefined; + return (selectedEntry?.task && !Types.isString(selectedEntry?.task)) ? this._toTask(selectedEntry?.task) : undefined; } } else if (firstLevelTask) { picker.dispose(); - return this.toTask(firstLevelTask); + return this._toTask(firstLevelTask); } else { picker.dispose(); return firstLevelTask; @@ -265,9 +289,11 @@ export class TaskQuickPick extends Disposable { return; } - private async doPickerFirstLevel(picker: IQuickPick<TaskTwoLevelQuickPickEntry>, taskQuickPickEntries: QuickPickInput<TaskTwoLevelQuickPickEntry>[]): Promise<Task | ConfiguringTask | string | null | undefined> { + + + private async _doPickerFirstLevel(picker: IQuickPick<ITaskTwoLevelQuickPickEntry>, taskQuickPickEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[]): Promise<Task | ConfiguringTask | string | null | undefined> { picker.items = taskQuickPickEntries; - const firstLevelPickerResult = await new Promise<TaskTwoLevelQuickPickEntry | undefined | null>(resolve => { + const firstLevelPickerResult = await new Promise<ITaskTwoLevelQuickPickEntry | undefined | null>(resolve => { Event.once(picker.onDidAccept)(async () => { resolve(picker.selectedItems ? picker.selectedItems[0] : undefined); }); @@ -275,18 +301,18 @@ export class TaskQuickPick extends Disposable { return firstLevelPickerResult?.task; } - private async doPickerSecondLevel(picker: IQuickPick<TaskTwoLevelQuickPickEntry>, type: string) { + private async _doPickerSecondLevel(picker: IQuickPick<ITaskTwoLevelQuickPickEntry>, type: string) { picker.busy = true; if (type === SHOW_ALL) { - const items = (await this.taskService.tasks()).sort((a, b) => this.sorter.compare(a, b)).map(task => this.createTaskEntry(task)); - items.push(...TaskQuickPick.allSettingEntries(this.configurationService)); + const items = (await this._taskService.tasks()).sort((a, b) => this._sorter.compare(a, b)).map(task => this._createTaskEntry(task)); + items.push(...TaskQuickPick.allSettingEntries(this._configurationService)); picker.items = items; } else { picker.value = ''; - picker.items = await this.getEntriesForProvider(type); + picker.items = await this._getEntriesForProvider(type); } picker.busy = false; - const secondLevelPickerResult = await new Promise<TaskTwoLevelQuickPickEntry | undefined | null>(resolve => { + const secondLevelPickerResult = await new Promise<ITaskTwoLevelQuickPickEntry | undefined | null>(resolve => { Event.once(picker.onDidAccept)(async () => { resolve(picker.selectedItems ? picker.selectedItems[0] : undefined); }); @@ -295,8 +321,8 @@ export class TaskQuickPick extends Disposable { return secondLevelPickerResult; } - public static allSettingEntries(configurationService: IConfigurationService): (TaskTwoLevelQuickPickEntry & { settingType: string })[] { - const entries: (TaskTwoLevelQuickPickEntry & { settingType: string })[] = []; + public static allSettingEntries(configurationService: IConfigurationService): (ITaskTwoLevelQuickPickEntry & { settingType: string })[] { + const entries: (ITaskTwoLevelQuickPickEntry & { settingType: string })[] = []; const gruntEntry = TaskQuickPick.getSettingEntry(configurationService, 'grunt'); if (gruntEntry) { entries.push(gruntEntry); @@ -312,7 +338,7 @@ export class TaskQuickPick extends Disposable { return entries; } - public static getSettingEntry(configurationService: IConfigurationService, type: string): (TaskTwoLevelQuickPickEntry & { settingType: string }) | undefined { + public static getSettingEntry(configurationService: IConfigurationService, type: string): (ITaskTwoLevelQuickPickEntry & { settingType: string }) | undefined { if (configurationService.getValue(`${type}.autoDetect`) === 'off') { return { label: nls.localize('TaskQuickPick.changeSettingsOptions', "$(gear) {0} task detection is turned off. Enable {1} task detection...", @@ -325,11 +351,11 @@ export class TaskQuickPick extends Disposable { return undefined; } - private async getEntriesForProvider(type: string): Promise<QuickPickInput<TaskTwoLevelQuickPickEntry>[]> { - const tasks = (await this.taskService.tasks({ type })).sort((a, b) => this.sorter.compare(a, b)); - let taskQuickPickEntries: QuickPickInput<TaskTwoLevelQuickPickEntry>[]; + private async _getEntriesForProvider(type: string): Promise<QuickPickInput<ITaskTwoLevelQuickPickEntry>[]> { + const tasks = (await this._taskService.tasks({ type })).sort((a, b) => this._sorter.compare(a, b)); + let taskQuickPickEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[]; if (tasks.length > 0) { - taskQuickPickEntries = tasks.map(task => this.createTaskEntry(task)); + taskQuickPickEntries = tasks.map(task => this._createTaskEntry(task)); taskQuickPickEntries.push({ type: 'separator' }, { @@ -345,30 +371,30 @@ export class TaskQuickPick extends Disposable { }]; } - const settingEntry = TaskQuickPick.getSettingEntry(this.configurationService, type); + const settingEntry = TaskQuickPick.getSettingEntry(this._configurationService, type); if (settingEntry) { taskQuickPickEntries.push(settingEntry); } return taskQuickPickEntries; } - private async toTask(task: Task | ConfiguringTask): Promise<Task | undefined> { + private async _toTask(task: Task | ConfiguringTask): Promise<Task | undefined> { if (!ConfiguringTask.is(task)) { return task; } - const resolvedTask = await this.taskService.tryResolveTask(task); + const resolvedTask = await this._taskService.tryResolveTask(task); if (!resolvedTask) { - this.notificationService.error(nls.localize('noProviderForTask', "There is no task provider registered for tasks of type \"{0}\".", task.type)); + this._notificationService.error(nls.localize('noProviderForTask', "There is no task provider registered for tasks of type \"{0}\".", task.type)); } return resolvedTask; } static async show(taskService: ITaskService, configurationService: IConfigurationService, quickInputService: IQuickInputService, notificationService: INotificationService, - dialogService: IDialogService, placeHolder: string, defaultEntry?: TaskQuickPickEntry) { - const taskQuickPick = new TaskQuickPick(taskService, configurationService, quickInputService, notificationService, dialogService); + dialogService: IDialogService, themeService: IThemeService, placeHolder: string, defaultEntry?: ITaskQuickPickEntry) { + const taskQuickPick = new TaskQuickPick(taskService, configurationService, quickInputService, notificationService, themeService, dialogService); return taskQuickPick.show(placeHolder, defaultEntry); } } diff --git a/src/vs/workbench/contrib/tasks/browser/taskService.ts b/src/vs/workbench/contrib/tasks/browser/taskService.ts index e34ad9e5794..efb6f0bb872 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskService.ts @@ -7,19 +7,19 @@ import * as nls from 'vs/nls'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ITaskSystem } from 'vs/workbench/contrib/tasks/common/taskSystem'; import { ExecutionEngine } from 'vs/workbench/contrib/tasks/common/tasks'; -import { AbstractTaskService, WorkspaceFolderConfigurationResult } from 'vs/workbench/contrib/tasks/browser/abstractTaskService'; -import { TaskFilter, ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; +import { AbstractTaskService, IWorkspaceFolderConfigurationResult } from 'vs/workbench/contrib/tasks/browser/abstractTaskService'; +import { ITaskFilter, ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class TaskService extends AbstractTaskService { private static readonly ProcessTaskSystemSupportMessage = nls.localize('taskService.processTaskSystem', 'Process task system is not support in the web.'); - protected getTaskSystem(): ITaskSystem { + protected _getTaskSystem(): ITaskSystem { if (this._taskSystem) { return this._taskSystem; } if (this.executionEngine === ExecutionEngine.Terminal) { - this._taskSystem = this.createTerminalTaskSystem(); + this._taskSystem = this._createTerminalTaskSystem(); } else { throw new Error(TaskService.ProcessTaskSystemSupportMessage); } @@ -32,11 +32,11 @@ export class TaskService extends AbstractTaskService { return this._taskSystem!; } - protected computeLegacyConfiguration(workspaceFolder: IWorkspaceFolder): Promise<WorkspaceFolderConfigurationResult> { + protected _computeLegacyConfiguration(workspaceFolder: IWorkspaceFolder): Promise<IWorkspaceFolderConfigurationResult> { throw new Error(TaskService.ProcessTaskSystemSupportMessage); } - protected versionAndEngineCompatible(filter?: TaskFilter): boolean { + protected _versionAndEngineCompatible(filter?: ITaskFilter): boolean { return this.executionEngine === ExecutionEngine.Terminal; } } diff --git a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts index ced0fd8cf8f..47d1f2a6486 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts @@ -5,27 +5,31 @@ import * as nls from 'vs/nls'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { AbstractProblemCollector, StartStopProblemCollector } from 'vs/workbench/contrib/tasks/common/problemCollectors'; -import { TaskEvent, TaskEventKind } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskEvent, TaskEventKind, TaskRunType } from 'vs/workbench/contrib/tasks/common/tasks'; import { ITaskService, Task } from 'vs/workbench/contrib/tasks/common/taskService'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalStatus } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { spinningLoading } from 'vs/platform/theme/common/iconRegistry'; +import { IMarker } from 'vs/platform/terminal/common/capabilities/capabilities'; -interface TerminalData { +interface ITerminalData { terminal: ITerminalInstance; + task: Task; status: ITerminalStatus; problemMatcher: AbstractProblemCollector; + taskRunEnded: boolean; + disposeListener?: IDisposable; } const TASK_TERMINAL_STATUS_ID = 'task_terminal_status'; -const ACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: spinningLoading, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.active', "Task is running") }; -const SUCCEEDED_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.check, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.succeeded', "Task succeeded") }; +export const ACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: spinningLoading, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.active', "Task is running") }; +export const SUCCEEDED_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.check, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.succeeded', "Task succeeded") }; const SUCCEEDED_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.check, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.succeededInactive', "Task succeeded and waiting...") }; -const FAILED_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.error, severity: Severity.Error, tooltip: nls.localize('taskTerminalStatus.errors', "Task has errors") }; +export const FAILED_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.error, severity: Severity.Error, tooltip: nls.localize('taskTerminalStatus.errors', "Task has errors") }; const FAILED_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.error, severity: Severity.Error, tooltip: nls.localize('taskTerminalStatus.errorsInactive', "Task has errors and is waiting...") }; const WARNING_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.warning, severity: Severity.Warning, tooltip: nls.localize('taskTerminalStatus.warnings', "Task has warnings") }; const WARNING_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.warning, severity: Severity.Warning, tooltip: nls.localize('taskTerminalStatus.warningsInactive', "Task has warnings and is waiting...") }; @@ -33,8 +37,8 @@ const INFO_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: C const INFO_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.info, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.infosInactive', "Task has infos and is waiting...") }; export class TaskTerminalStatus extends Disposable { - private terminalMap: Map<Task, TerminalData> = new Map(); - + private terminalMap: Map<string, ITerminalData> = new Map(); + private _marker: IMarker | undefined; constructor(taskService: ITaskService) { super(); this._register(taskService.onDidStateChange((event) => { @@ -50,29 +54,45 @@ export class TaskTerminalStatus extends Disposable { addTerminal(task: Task, terminal: ITerminalInstance, problemMatcher: AbstractProblemCollector) { const status: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, severity: Severity.Info }; terminal.statusList.add(status); - this.terminalMap.set(task, { terminal, status, problemMatcher }); + problemMatcher.onDidFindFirstMatch(() => { + this._marker = terminal.registerMarker(); + }); + problemMatcher.onDidFindErrors(() => { + if (this._marker) { + terminal.addGenericMark(this._marker, { hoverMessage: nls.localize('task.watchFirstError', "Beginning of detected errors for this run"), disableCommandStorage: true }); + } + }); + problemMatcher.onDidRequestInvalidateLastMarker(() => { + this._marker?.dispose(); + this._marker = undefined; + }); + this.terminalMap.set(task._id, { terminal, task, status, problemMatcher, taskRunEnded: false }); } - private terminalFromEvent(event: TaskEvent): TerminalData | undefined { - if (!event.__task || !this.terminalMap.get(event.__task)) { + private terminalFromEvent(event: ITaskEvent): ITerminalData | undefined { + if (!event.__task) { return undefined; } - return this.terminalMap.get(event.__task); + return this.terminalMap.get(event.__task._id); } - private eventEnd(event: TaskEvent) { + private eventEnd(event: ITaskEvent) { const terminalData = this.terminalFromEvent(event); if (!terminalData) { return; } - - this.terminalMap.delete(event.__task!); - + terminalData.taskRunEnded = true; terminalData.terminal.statusList.remove(terminalData.status); if ((event.exitCode === 0) && (terminalData.problemMatcher.numberOfMatches === 0)) { - terminalData.terminal.statusList.add(SUCCEEDED_TASK_STATUS); - } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Error) { + if (terminalData.task.configurationProperties.isBackground) { + for (const status of terminalData.terminal.statusList.statuses) { + terminalData.terminal.statusList.remove(status); + } + } else { + terminalData.terminal.statusList.add(SUCCEEDED_TASK_STATUS); + } + } else if (event.exitCode || terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Error) { terminalData.terminal.statusList.add(FAILED_TASK_STATUS); } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Warning) { terminalData.terminal.statusList.add(WARNING_TASK_STATUS); @@ -81,9 +101,9 @@ export class TaskTerminalStatus extends Disposable { } } - private eventInactive(event: TaskEvent) { + private eventInactive(event: ITaskEvent) { const terminalData = this.terminalFromEvent(event); - if (!terminalData || !terminalData.problemMatcher) { + if (!terminalData || !terminalData.problemMatcher || terminalData.taskRunEnded) { return; } terminalData.terminal.statusList.remove(terminalData.status); @@ -98,15 +118,21 @@ export class TaskTerminalStatus extends Disposable { } } - private eventActive(event: TaskEvent) { + private eventActive(event: ITaskEvent) { const terminalData = this.terminalFromEvent(event); if (!terminalData) { return; } - + if (!terminalData.disposeListener) { + terminalData.disposeListener = terminalData.terminal.onDisposed(() => { + this.terminalMap.delete(event.__task?._id!); + terminalData.disposeListener?.dispose(); + }); + } + terminalData.taskRunEnded = false; terminalData.terminal.statusList.remove(terminalData.status); // We don't want to show an infinite status for a background task that doesn't have a problem matcher. - if ((terminalData.problemMatcher instanceof StartStopProblemCollector) || (terminalData.problemMatcher?.problemMatchers.length > 0)) { + if ((terminalData.problemMatcher instanceof StartStopProblemCollector) || (terminalData.problemMatcher?.problemMatchers.length > 0) || event.runType === TaskRunType.SingleRun) { terminalData.terminal.statusList.add(ACTIVE_TASK_STATUS); } } diff --git a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts index 24b9b2d9779..7c3731d171d 100644 --- a/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts +++ b/src/vs/workbench/contrib/tasks/browser/tasksQuickAccess.ts @@ -12,11 +12,12 @@ import { ITaskService, Task } from 'vs/workbench/contrib/tasks/common/taskServic import { CustomTask, ContributedTask, ConfiguringTask } from 'vs/workbench/contrib/tasks/common/tasks'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { TaskQuickPick, TaskTwoLevelQuickPickEntry } from 'vs/workbench/contrib/tasks/browser/taskQuickPick'; +import { TaskQuickPick, ITaskTwoLevelQuickPickEntry } from 'vs/workbench/contrib/tasks/browser/taskQuickPick'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isString } from 'vs/base/common/types'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQuickAccessItem> { @@ -24,11 +25,12 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQ constructor( @IExtensionService extensionService: IExtensionService, - @ITaskService private taskService: ITaskService, - @IConfigurationService private configurationService: IConfigurationService, - @IQuickInputService private quickInputService: IQuickInputService, - @INotificationService private notificationService: INotificationService, - @IDialogService private dialogService: IDialogService + @ITaskService private _taskService: ITaskService, + @IConfigurationService private _configurationService: IConfigurationService, + @IQuickInputService private _quickInputService: IQuickInputService, + @INotificationService private _notificationService: INotificationService, + @IDialogService private _dialogService: IDialogService, + @IThemeService private _themeService: IThemeService ) { super(TasksQuickAccessProvider.PREFIX, { noResultsPick: { @@ -42,7 +44,7 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQ return []; } - const taskQuickPick = new TaskQuickPick(this.taskService, this.configurationService, this.quickInputService, this.notificationService, this.dialogService); + const taskQuickPick = new TaskQuickPick(this._taskService, this._configurationService, this._quickInputService, this._notificationService, this._themeService, this._dialogService); const topLevelPicks = await taskQuickPick.getTopLevelEntries(); const taskPicks: Array<IPickerQuickAccessItem | IQuickPickSeparator> = []; @@ -56,21 +58,21 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQ taskPicks.push(entry); } - const task: Task | ConfiguringTask | string = (<TaskTwoLevelQuickPickEntry>entry).task!; - const quickAccessEntry: IPickerQuickAccessItem = <TaskTwoLevelQuickPickEntry>entry; + const task: Task | ConfiguringTask | string = (<ITaskTwoLevelQuickPickEntry>entry).task!; + const quickAccessEntry: IPickerQuickAccessItem = <ITaskTwoLevelQuickPickEntry>entry; quickAccessEntry.highlights = { label: highlights }; quickAccessEntry.trigger = (index) => { if ((index === 1) && (quickAccessEntry.buttons?.length === 2)) { const key = (task && !isString(task)) ? task.getRecentlyUsedKey() : undefined; if (key) { - this.taskService.removeRecentlyUsedTask(key); + this._taskService.removeRecentlyUsedTask(key); } return TriggerAction.REFRESH_PICKER; } else { if (ContributedTask.is(task)) { - this.taskService.customize(task, undefined, true); + this._taskService.customize(task, undefined, true); } else if (CustomTask.is(task)) { - this.taskService.openConfig(task); + this._taskService.openConfig(task); } return TriggerAction.CLOSE_PICKER; } @@ -80,10 +82,10 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQ // switch to quick pick and show second level const showResult = await taskQuickPick.show(localize('TaskService.pickRunTask', 'Select the task to run'), undefined, task); if (showResult) { - this.taskService.run(showResult, { attachProblemMatcher: true }); + this._taskService.run(showResult, { attachProblemMatcher: true }); } } else { - this.taskService.run(await this.toTask(task), { attachProblemMatcher: true }); + this._taskService.run(await this._toTask(task), { attachProblemMatcher: true }); } }; @@ -92,11 +94,11 @@ export class TasksQuickAccessProvider extends PickerQuickAccessProvider<IPickerQ return taskPicks; } - private async toTask(task: Task | ConfiguringTask): Promise<Task | undefined> { + private async _toTask(task: Task | ConfiguringTask): Promise<Task | undefined> { if (!ConfiguringTask.is(task)) { return task; } - return this.taskService.tryResolveTask(task); + return this._taskService.tryResolveTask(task); } } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 6cb1d3b7f86..d1dd693f370 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -10,7 +10,7 @@ import * as Types from 'vs/base/common/types'; import * as Platform from 'vs/base/common/platform'; import * as Async from 'vs/base/common/async'; import * as resources from 'vs/base/common/resources'; -import { IStringDictionary, values } from 'vs/base/common/collections'; +import { IStringDictionary } from 'vs/base/common/collections'; import { LinkedMap, Touch } from 'vs/base/common/map'; import Severity from 'vs/base/common/severity'; import { Event, Emitter } from 'vs/base/common/event'; @@ -22,7 +22,7 @@ import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/marke import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IModelService } from 'vs/editor/common/services/model'; import { ProblemMatcher, ProblemMatcherRegistry /*, ProblemPattern, getResource */ } from 'vs/workbench/contrib/tasks/common/problemMatcher'; -import Constants from 'vs/workbench/contrib/markers/browser/constants'; +import { Markers } from 'vs/workbench/contrib/markers/common/markers'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -30,12 +30,12 @@ import { ITerminalService, ITerminalInstance, ITerminalGroupService } from 'vs/w import { IOutputService } from 'vs/workbench/services/output/common/output'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { - Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind, - TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration, ExtensionTaskSource, TaskScope, RevealProblemKind, DependsOrder, TaskSourceKind, InMemoryTask + Task, CustomTask, ContributedTask, RevealKind, CommandOptions, IShellConfiguration, RuntimeType, PanelKind, + TaskEvent, TaskEventKind, IShellQuotingOptions, ShellQuoting, CommandString, ICommandConfiguration, IExtensionTaskSource, TaskScope, RevealProblemKind, DependsOrder, TaskSourceKind, InMemoryTask, ITaskEvent } from 'vs/workbench/contrib/tasks/common/tasks'; import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver, - Triggers, TaskTerminateResponse, TaskSystemInfoResolver, TaskSystemInfo, ResolveSet, ResolvedVariables + Triggers, ITaskTerminateResponse, ITaskSystemInfoResolver, ITaskSystemInfo, IResolveSet, IResolvedVariables } from 'vs/workbench/contrib/tasks/common/taskSystem'; import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -50,14 +50,19 @@ import { TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTermi import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; +import { GroupKind } from 'vs/workbench/contrib/tasks/common/taskConfiguration'; +import { Codicon } from 'vs/base/common/codicons'; +import { VSCodeOscProperty, VSCodeOscPt, VSCodeSequence } from 'vs/workbench/contrib/terminal/browser/terminalEscapeSequences'; -interface TerminalData { +interface ITerminalData { terminal: ITerminalInstance; lastTask: string; group?: string; } -interface ActiveTerminalData { +interface IActiveTerminalData { terminal: ITerminalInstance; task: Task; promise: Promise<ITaskSummary>; @@ -84,23 +89,23 @@ class InstanceManager { } class VariableResolver { - private static regex = /\$\{(.*?)\}/g; - constructor(public workspaceFolder: IWorkspaceFolder | undefined, public taskSystemInfo: TaskSystemInfo | undefined, public readonly values: Map<string, string>, private _service: IConfigurationResolverService | undefined) { + private static _regex = /\$\{(.*?)\}/g; + constructor(public workspaceFolder: IWorkspaceFolder | undefined, public taskSystemInfo: ITaskSystemInfo | undefined, public readonly values: Map<string, string>, private _service: IConfigurationResolverService | undefined) { } async resolve(value: string): Promise<string> { const replacers: Promise<string>[] = []; - value.replace(VariableResolver.regex, (match, ...args) => { - replacers.push(this.replacer(match, args)); + value.replace(VariableResolver._regex, (match, ...args) => { + replacers.push(this._replacer(match, args)); return match; }); const resolvedReplacers = await Promise.all(replacers); - return value.replace(VariableResolver.regex, () => resolvedReplacers.shift()!); + return value.replace(VariableResolver._regex, () => resolvedReplacers.shift()!); } - private async replacer(match: string, args: string[]): Promise<string> { + private async _replacer(match: string, args: string[]): Promise<string> { // Strip out the ${} because the map contains them variables without those characters. - let result = this.values.get(match.substring(2, match.length - 1)); + const result = this.values.get(match.substring(2, match.length - 1)); if ((result !== undefined) && (result !== null)) { return result; } @@ -115,8 +120,8 @@ export class VerifiedTask { readonly task: Task; readonly resolver: ITaskResolver; readonly trigger: string; - resolvedVariables?: ResolvedVariables; - systemInfo?: TaskSystemInfo; + resolvedVariables?: IResolvedVariables; + systemInfo?: ITaskSystemInfo; workspaceFolder?: IWorkspaceFolder; shellLaunchConfig?: IShellLaunchConfig; @@ -134,7 +139,7 @@ export class VerifiedTask { return verified; } - public getVerifiedTask(): { task: Task; resolver: ITaskResolver; trigger: string; resolvedVariables: ResolvedVariables; systemInfo: TaskSystemInfo; workspaceFolder: IWorkspaceFolder; shellLaunchConfig: IShellLaunchConfig } { + public getVerifiedTask(): { task: Task; resolver: ITaskResolver; trigger: string; resolvedVariables: IResolvedVariables; systemInfo: ITaskSystemInfo; workspaceFolder: IWorkspaceFolder; shellLaunchConfig: IShellLaunchConfig } { if (this.verify()) { return { task: this.task, resolver: this.resolver, trigger: this.trigger, resolvedVariables: this.resolvedVariables!, systemInfo: this.systemInfo!, workspaceFolder: this.workspaceFolder!, shellLaunchConfig: this.shellLaunchConfig! }; } else { @@ -149,7 +154,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { private static readonly ProcessVarName = '__process__'; - private static shellQuotes: IStringDictionary<ShellQuotingOptions> = { + private static _shellQuotes: IStringDictionary<IShellQuotingOptions> = { 'cmd': { strong: '"' }, @@ -179,126 +184,134 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } }; - private static osShellQuotes: IStringDictionary<ShellQuotingOptions> = { - 'Linux': TerminalTaskSystem.shellQuotes['bash'], - 'Mac': TerminalTaskSystem.shellQuotes['bash'], - 'Windows': TerminalTaskSystem.shellQuotes['powershell'] + private static _osShellQuotes: IStringDictionary<IShellQuotingOptions> = { + 'Linux': TerminalTaskSystem._shellQuotes['bash'], + 'Mac': TerminalTaskSystem._shellQuotes['bash'], + 'Windows': TerminalTaskSystem._shellQuotes['powershell'] }; - private activeTasks: IStringDictionary<ActiveTerminalData>; - private instances: IStringDictionary<InstanceManager>; - private busyTasks: IStringDictionary<Task>; - private terminals: IStringDictionary<TerminalData>; - private idleTaskTerminals: LinkedMap<string, string>; - private sameTaskTerminals: IStringDictionary<string>; - private taskSystemInfoResolver: TaskSystemInfoResolver; - private lastTask: VerifiedTask | undefined; + private _activeTasks: IStringDictionary<IActiveTerminalData>; + private _instances: IStringDictionary<InstanceManager>; + private _busyTasks: IStringDictionary<Task>; + private _terminals: IStringDictionary<ITerminalData>; + private _idleTaskTerminals: LinkedMap<string, string>; + private _sameTaskTerminals: IStringDictionary<string>; + private _taskSystemInfoResolver: ITaskSystemInfoResolver; + private _lastTask: VerifiedTask | undefined; // Should always be set in run - private currentTask!: VerifiedTask; - private isRerun: boolean = false; - private previousPanelId: string | undefined; - private previousTerminalInstance: ITerminalInstance | undefined; - private terminalStatusManager: TaskTerminalStatus; - private terminalCreationQueue: Promise<ITerminalInstance | void> = Promise.resolve(); + private _currentTask!: VerifiedTask; + private _isRerun: boolean = false; + private _previousPanelId: string | undefined; + private _previousTerminalInstance: ITerminalInstance | undefined; + private _terminalStatusManager: TaskTerminalStatus; + private _terminalCreationQueue: Promise<ITerminalInstance | void> = Promise.resolve(); - private readonly _onDidStateChange: Emitter<TaskEvent>; + private readonly _onDidStateChange: Emitter<ITaskEvent>; + + get taskShellIntegrationStartSequence(): string { + return this._configurationService.getValue('task.showDecorations') ? VSCodeSequence(VSCodeOscPt.PromptStart) + VSCodeSequence(VSCodeOscPt.Property, `${VSCodeOscProperty.Task}=True`) + VSCodeSequence(VSCodeOscPt.CommandStart) : ''; + } + get taskShellIntegrationOutputSequence(): string { + return this._configurationService.getValue('task.showDecorations') ? VSCodeSequence(VSCodeOscPt.CommandExecuted) : ''; + } constructor( - private terminalService: ITerminalService, - private terminalGroupService: ITerminalGroupService, - private outputService: IOutputService, - private paneCompositeService: IPaneCompositePartService, - private viewsService: IViewsService, - private markerService: IMarkerService, private modelService: IModelService, - private configurationResolverService: IConfigurationResolverService, - private contextService: IWorkspaceContextService, - private environmentService: IWorkbenchEnvironmentService, - private outputChannelId: string, - private fileService: IFileService, - private terminalProfileResolverService: ITerminalProfileResolverService, - private pathService: IPathService, - private viewDescriptorService: IViewDescriptorService, - private logService: ILogService, - private configurationService: IConfigurationService, - private notificationService: INotificationService, + private _terminalService: ITerminalService, + private _terminalGroupService: ITerminalGroupService, + private _outputService: IOutputService, + private _paneCompositeService: IPaneCompositePartService, + private _viewsService: IViewsService, + private _markerService: IMarkerService, + private _modelService: IModelService, + private _configurationResolverService: IConfigurationResolverService, + private _contextService: IWorkspaceContextService, + private _environmentService: IWorkbenchEnvironmentService, + private _outputChannelId: string, + private _fileService: IFileService, + private _terminalProfileResolverService: ITerminalProfileResolverService, + private _pathService: IPathService, + private _viewDescriptorService: IViewDescriptorService, + private _logService: ILogService, + private _configurationService: IConfigurationService, + private _notificationService: INotificationService, taskService: ITaskService, - taskSystemInfoResolver: TaskSystemInfoResolver, + taskSystemInfoResolver: ITaskSystemInfoResolver, ) { super(); - this.activeTasks = Object.create(null); - this.instances = Object.create(null); - this.busyTasks = Object.create(null); - this.terminals = Object.create(null); - this.idleTaskTerminals = new LinkedMap<string, string>(); - this.sameTaskTerminals = Object.create(null); + this._activeTasks = Object.create(null); + this._instances = Object.create(null); + this._busyTasks = Object.create(null); + this._terminals = Object.create(null); + this._idleTaskTerminals = new LinkedMap<string, string>(); + this._sameTaskTerminals = Object.create(null); this._onDidStateChange = new Emitter(); - this.taskSystemInfoResolver = taskSystemInfoResolver; - this._register(this.terminalStatusManager = new TaskTerminalStatus(taskService)); + this._taskSystemInfoResolver = taskSystemInfoResolver; + this._register(this._terminalStatusManager = new TaskTerminalStatus(taskService)); } - public get onDidStateChange(): Event<TaskEvent> { + public get onDidStateChange(): Event<ITaskEvent> { return this._onDidStateChange.event; } - private log(value: string): void { - this.appendOutput(value + '\n'); + private _log(value: string): void { + this._appendOutput(value + '\n'); } - protected showOutput(): void { - this.outputService.showChannel(this.outputChannelId, true); + protected _showOutput(): void { + this._outputService.showChannel(this._outputChannelId, true); } public run(task: Task, resolver: ITaskResolver, trigger: string = Triggers.command): ITaskExecuteResult { task = task.clone(); // A small amount of task state is stored in the task (instance) and tasks passed in to run may have that set already. const recentTaskKey = task.getRecentlyUsedKey() ?? ''; - let validInstance = task.runOptions && task.runOptions.instanceLimit && this.instances[recentTaskKey] && this.instances[recentTaskKey].instances < task.runOptions.instanceLimit; - let instance = this.instances[recentTaskKey] ? this.instances[recentTaskKey].instances : 0; - this.currentTask = new VerifiedTask(task, resolver, trigger); + const validInstance = task.runOptions && task.runOptions.instanceLimit && this._instances[recentTaskKey] && this._instances[recentTaskKey].instances < task.runOptions.instanceLimit; + const instance = this._instances[recentTaskKey] ? this._instances[recentTaskKey].instances : 0; + this._currentTask = new VerifiedTask(task, resolver, trigger); if (instance > 0) { - task.instance = this.instances[recentTaskKey].counter; + task.instance = this._instances[recentTaskKey].counter; } - let lastTaskInstance = this.getLastInstance(task); - let terminalData = lastTaskInstance ? this.activeTasks[lastTaskInstance.getMapKey()] : undefined; + const lastTaskInstance = this.getLastInstance(task); + const terminalData = lastTaskInstance ? this._activeTasks[lastTaskInstance.getMapKey()] : undefined; if (terminalData && terminalData.promise && !validInstance) { - this.lastTask = this.currentTask; + this._lastTask = this._currentTask; return { kind: TaskExecuteKind.Active, task: terminalData.task, active: { same: true, background: task.configurationProperties.isBackground! }, promise: terminalData.promise }; } try { - const executeResult = { kind: TaskExecuteKind.Started, task, started: {}, promise: this.executeTask(task, resolver, trigger, new Set()) }; + const executeResult = { kind: TaskExecuteKind.Started, task, started: {}, promise: this._executeTask(task, resolver, trigger, new Set()) }; executeResult.promise.then(summary => { - this.lastTask = this.currentTask; + this._lastTask = this._currentTask; }); - if (InMemoryTask.is(task) || !this.isTaskEmpty(task)) { - if (!this.instances[recentTaskKey]) { - this.instances[recentTaskKey] = new InstanceManager(); + if (InMemoryTask.is(task) || !this._isTaskEmpty(task)) { + if (!this._instances[recentTaskKey]) { + this._instances[recentTaskKey] = new InstanceManager(); } - this.instances[recentTaskKey].addInstance(); + this._instances[recentTaskKey].addInstance(); } return executeResult; } catch (error) { if (error instanceof TaskError) { throw error; } else if (error instanceof Error) { - this.log(error.message); + this._log(error.message); throw new TaskError(Severity.Error, error.message, TaskErrors.UnknownError); } else { - this.log(error.toString()); + this._log(error.toString()); throw new TaskError(Severity.Error, nls.localize('TerminalTaskSystem.unknownError', 'A unknown error has occurred while executing a task. See task output log for details.'), TaskErrors.UnknownError); } } } public rerun(): ITaskExecuteResult | undefined { - if (this.lastTask && this.lastTask.verify()) { - if ((this.lastTask.task.runOptions.reevaluateOnRerun !== undefined) && !this.lastTask.task.runOptions.reevaluateOnRerun) { - this.isRerun = true; + if (this._lastTask && this._lastTask.verify()) { + if ((this._lastTask.task.runOptions.reevaluateOnRerun !== undefined) && !this._lastTask.task.runOptions.reevaluateOnRerun) { + this._isRerun = true; } - const result = this.run(this.lastTask.task, this.lastTask.resolver); + const result = this.run(this._lastTask.task, this._lastTask.resolver); result.promise.then(summary => { - this.isRerun = false; + this._isRerun = false; }); return result; } else { @@ -306,59 +319,59 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } - private showTaskLoadErrors(task: Task) { + private _showTaskLoadErrors(task: Task) { if (task.taskLoadMessages && task.taskLoadMessages.length > 0) { task.taskLoadMessages.forEach(loadMessage => { - this.log(loadMessage + '\n'); + this._log(loadMessage + '\n'); }); const openOutput = 'Show Output'; - this.notificationService.prompt(Severity.Warning, + this._notificationService.prompt(Severity.Warning, nls.localize('TerminalTaskSystem.taskLoadReporting', "There are issues with task \"{0}\". See the output for more details.", task._label), [{ label: openOutput, - run: () => this.showOutput() + run: () => this._showOutput() }]); } } public isTaskVisible(task: Task): boolean { - let terminalData = this.activeTasks[task.getMapKey()]; + const terminalData = this._activeTasks[task.getMapKey()]; if (!terminalData) { return false; } - const activeTerminalInstance = this.terminalService.activeInstance; - const isPanelShowingTerminal = !!this.viewsService.getActiveViewWithId(TERMINAL_VIEW_ID); + const activeTerminalInstance = this._terminalService.activeInstance; + const isPanelShowingTerminal = !!this._viewsService.getActiveViewWithId(TERMINAL_VIEW_ID); return isPanelShowingTerminal && (activeTerminalInstance?.instanceId === terminalData.terminal.instanceId); } public revealTask(task: Task): boolean { - let terminalData = this.activeTasks[task.getMapKey()]; + const terminalData = this._activeTasks[task.getMapKey()]; if (!terminalData) { return false; } - const isTerminalInPanel: boolean = this.viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID) === ViewContainerLocation.Panel; + const isTerminalInPanel: boolean = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID) === ViewContainerLocation.Panel; if (isTerminalInPanel && this.isTaskVisible(task)) { - if (this.previousPanelId) { - if (this.previousTerminalInstance) { - this.terminalService.setActiveInstance(this.previousTerminalInstance); + if (this._previousPanelId) { + if (this._previousTerminalInstance) { + this._terminalService.setActiveInstance(this._previousTerminalInstance); } - this.paneCompositeService.openPaneComposite(this.previousPanelId, ViewContainerLocation.Panel); + this._paneCompositeService.openPaneComposite(this._previousPanelId, ViewContainerLocation.Panel); } else { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); + this._paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); } - this.previousPanelId = undefined; - this.previousTerminalInstance = undefined; + this._previousPanelId = undefined; + this._previousTerminalInstance = undefined; } else { if (isTerminalInPanel) { - this.previousPanelId = this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)?.getId(); - if (this.previousPanelId === TERMINAL_VIEW_ID) { - this.previousTerminalInstance = this.terminalService.activeInstance ?? undefined; + this._previousPanelId = this._paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)?.getId(); + if (this._previousPanelId === TERMINAL_VIEW_ID) { + this._previousTerminalInstance = this._terminalService.activeInstance ?? undefined; } } - this.terminalService.setActiveInstance(terminalData.terminal); + this._terminalService.setActiveInstance(terminalData.terminal); if (CustomTask.is(task) || ContributedTask.is(task)) { - this.terminalGroupService.showPanel(task.command.presentation!.focus); + this._terminalGroupService.showPanel(task.command.presentation!.focus); } } return true; @@ -369,34 +382,34 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } public isActiveSync(): boolean { - return Object.keys(this.activeTasks).length > 0; + return Object.keys(this._activeTasks).length > 0; } public canAutoTerminate(): boolean { - return Object.keys(this.activeTasks).every(key => !this.activeTasks[key].task.configurationProperties.promptOnClose); + return Object.keys(this._activeTasks).every(key => !this._activeTasks[key].task.configurationProperties.promptOnClose); } public getActiveTasks(): Task[] { - return Object.keys(this.activeTasks).map(key => this.activeTasks[key].task); + return Object.keys(this._activeTasks).map(key => this._activeTasks[key].task); } public getLastInstance(task: Task): Task | undefined { let lastInstance = undefined; const recentKey = task.getRecentlyUsedKey(); - Object.keys(this.activeTasks).forEach((key) => { - if (recentKey && recentKey === this.activeTasks[key].task.getRecentlyUsedKey()) { - lastInstance = this.activeTasks[key].task; + Object.keys(this._activeTasks).forEach((key) => { + if (recentKey && recentKey === this._activeTasks[key].task.getRecentlyUsedKey()) { + lastInstance = this._activeTasks[key].task; } }); return lastInstance; } public getBusyTasks(): Task[] { - return Object.keys(this.busyTasks).map(key => this.busyTasks[key]); + return Object.keys(this._busyTasks).map(key => this._busyTasks[key]); } public customExecutionComplete(task: Task, result: number): Promise<void> { - let activeTerminal = this.activeTasks[task.getMapKey()]; + const activeTerminal = this._activeTasks[task.getMapKey()]; if (!activeTerminal) { return Promise.reject(new Error('Expected to have a terminal for an custom execution task')); } @@ -407,27 +420,27 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }); } - private removeInstances(task: Task) { + private _removeInstances(task: Task) { const recentTaskKey = task.getRecentlyUsedKey() ?? ''; - if (this.instances[recentTaskKey]) { - this.instances[recentTaskKey].removeInstance(); - if (this.instances[recentTaskKey].instances === 0) { - delete this.instances[recentTaskKey]; + if (this._instances[recentTaskKey]) { + this._instances[recentTaskKey].removeInstance(); + if (this._instances[recentTaskKey].instances === 0) { + delete this._instances[recentTaskKey]; } } } - private removeFromActiveTasks(task: Task): void { - if (!this.activeTasks[task.getMapKey()]) { + private _removeFromActiveTasks(task: Task): void { + if (!this._activeTasks[task.getMapKey()]) { return; } - delete this.activeTasks[task.getMapKey()]; - this.removeInstances(task); + delete this._activeTasks[task.getMapKey()]; + this._removeInstances(task); } - private fireTaskEvent(event: TaskEvent) { + private _fireTaskEvent(event: ITaskEvent) { if (event.__task) { - const activeTask = this.activeTasks[event.__task.getMapKey()]; + const activeTask = this._activeTasks[event.__task.getMapKey()]; if (activeTask) { activeTask.state = event.kind; } @@ -435,19 +448,19 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this._onDidStateChange.fire(event); } - public terminate(task: Task): Promise<TaskTerminateResponse> { - let activeTerminal = this.activeTasks[task.getMapKey()]; + public terminate(task: Task): Promise<ITaskTerminateResponse> { + const activeTerminal = this._activeTasks[task.getMapKey()]; if (!activeTerminal) { - return Promise.resolve<TaskTerminateResponse>({ success: false, task: undefined }); + return Promise.resolve<ITaskTerminateResponse>({ success: false, task: undefined }); } - return new Promise<TaskTerminateResponse>((resolve, reject) => { - let terminal = activeTerminal.terminal; + return new Promise<ITaskTerminateResponse>((resolve, reject) => { + const terminal = activeTerminal.terminal; const onExit = terminal.onExit(() => { - let task = activeTerminal.task; + const task = activeTerminal.task; try { onExit.dispose(); - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Terminated, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Terminated, task)); } catch (error) { // Do nothing. } @@ -457,17 +470,17 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }); } - public terminateAll(): Promise<TaskTerminateResponse[]> { - let promises: Promise<TaskTerminateResponse>[] = []; - Object.keys(this.activeTasks).forEach((key) => { - let terminalData = this.activeTasks[key]; - let terminal = terminalData.terminal; - promises.push(new Promise<TaskTerminateResponse>((resolve, reject) => { + public terminateAll(): Promise<ITaskTerminateResponse[]> { + const promises: Promise<ITaskTerminateResponse>[] = []; + Object.keys(this._activeTasks).forEach((key) => { + const terminalData = this._activeTasks[key]; + const terminal = terminalData.terminal; + promises.push(new Promise<ITaskTerminateResponse>((resolve, reject) => { const onExit = terminal.onExit(() => { - let task = terminalData.task; + const task = terminalData.task; try { onExit.dispose(); - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Terminated, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Terminated, task)); } catch (error) { // Do nothing. } @@ -476,39 +489,45 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { })); terminal.dispose(); }); - this.activeTasks = Object.create(null); - return Promise.all<TaskTerminateResponse>(promises); + this._activeTasks = Object.create(null); + return Promise.all<ITaskTerminateResponse>(promises); } - private showDependencyCycleMessage(task: Task) { - this.log(nls.localize('dependencyCycle', + private _showDependencyCycleMessage(task: Task) { + this._log(nls.localize('dependencyCycle', 'There is a dependency cycle. See task "{0}".', task._label )); - this.showOutput(); + this._showOutput(); } - private async executeTask(task: Task, resolver: ITaskResolver, trigger: string, encounteredDependencies: Set<string>, alreadyResolved?: Map<string, string>): Promise<ITaskSummary> { + private async _executeTask(task: Task, resolver: ITaskResolver, trigger: string, encounteredDependencies: Set<string>, alreadyResolved?: Map<string, string>): Promise<ITaskSummary> { if (encounteredDependencies.has(task.getCommonTaskId())) { - this.showDependencyCycleMessage(task); + this._showDependencyCycleMessage(task); return {}; } - this.showTaskLoadErrors(task); + this._showTaskLoadErrors(task); alreadyResolved = alreadyResolved ?? new Map<string, string>(); - let promises: Promise<ITaskSummary>[] = []; + const promises: Promise<ITaskSummary>[] = []; if (task.configurationProperties.dependsOn) { for (const dependency of task.configurationProperties.dependsOn) { - let dependencyTask = await resolver.resolve(dependency.uri, dependency.task!); + const dependencyTask = await resolver.resolve(dependency.uri, dependency.task!); if (dependencyTask) { - let key = dependencyTask.getMapKey(); - let promise = this.activeTasks[key] ? this.getDependencyPromise(this.activeTasks[key]) : undefined; + if (dependencyTask.configurationProperties.icon) { + dependencyTask.configurationProperties.icon.id ||= task.configurationProperties.icon?.id; + dependencyTask.configurationProperties.icon.color ||= task.configurationProperties.icon?.color; + } else { + dependencyTask.configurationProperties.icon = task.configurationProperties.icon; + } + const key = dependencyTask.getMapKey(); + let promise = this._activeTasks[key] ? this._getDependencyPromise(this._activeTasks[key]) : undefined; if (!promise) { - this.fireTaskEvent(TaskEvent.create(TaskEventKind.DependsOnStarted, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.DependsOnStarted, task)); encounteredDependencies.add(task.getCommonTaskId()); - promise = this.executeDependencyTask(dependencyTask, resolver, trigger, encounteredDependencies, alreadyResolved); + promise = this._executeDependencyTask(dependencyTask, resolver, trigger, encounteredDependencies, alreadyResolved); } promises.push(promise); if (task.configurationProperties.dependsOrder === DependsOrder.sequence) { @@ -522,12 +541,12 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } promises.push(promise); } else { - this.log(nls.localize('dependencyFailed', + this._log(nls.localize('dependencyFailed', 'Couldn\'t resolve dependent task \'{0}\' in workspace folder \'{1}\'', Types.isString(dependency.task) ? dependency.task : JSON.stringify(dependency.task, undefined, 0), dependency.uri.toString() )); - this.showOutput(); + this._showOutput(); } } } @@ -535,22 +554,22 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if ((ContributedTask.is(task) || CustomTask.is(task)) && (task.command)) { return Promise.all(promises).then((summaries): Promise<ITaskSummary> | ITaskSummary => { encounteredDependencies.delete(task.getCommonTaskId()); - for (let summary of summaries) { + for (const summary of summaries) { if (summary.exitCode !== 0) { - this.removeInstances(task); + this._removeInstances(task); return { exitCode: summary.exitCode }; } } - if (this.isRerun) { - return this.reexecuteCommand(task, trigger, alreadyResolved!); + if (this._isRerun) { + return this._reexecuteCommand(task, trigger, alreadyResolved!); } else { - return this.executeCommand(task, trigger, alreadyResolved!); + return this._executeCommand(task, trigger, alreadyResolved!); } }); } else { return Promise.all(promises).then((summaries): ITaskSummary => { encounteredDependencies.delete(task.getCommonTaskId()); - for (let summary of summaries) { + for (const summary of summaries) { if (summary.exitCode !== 0) { return { exitCode: summary.exitCode }; } @@ -560,7 +579,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } - private createInactiveDependencyPromise(task: Task): Promise<ITaskSummary> { + private _createInactiveDependencyPromise(task: Task): Promise<ITaskSummary> { return new Promise<ITaskSummary>(resolve => { const taskInactiveDisposable = this.onDidStateChange(taskEvent => { if ((taskEvent.kind === TaskEventKind.Inactive) && (taskEvent.__task === task)) { @@ -571,7 +590,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }); } - private async getDependencyPromise(task: ActiveTerminalData): Promise<ITaskSummary> { + private async _getDependencyPromise(task: IActiveTerminalData): Promise<ITaskSummary> { if (!task.task.configurationProperties.isBackground) { return task.promise; } @@ -581,24 +600,24 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (task.state === TaskEventKind.Inactive) { return { exitCode: 0 }; } - return this.createInactiveDependencyPromise(task.task); + return this._createInactiveDependencyPromise(task.task); } - private async executeDependencyTask(task: Task, resolver: ITaskResolver, trigger: string, encounteredDependencies: Set<string>, alreadyResolved?: Map<string, string>): Promise<ITaskSummary> { + private async _executeDependencyTask(task: Task, resolver: ITaskResolver, trigger: string, encounteredDependencies: Set<string>, alreadyResolved?: Map<string, string>): Promise<ITaskSummary> { // If the task is a background task with a watching problem matcher, we don't wait for the whole task to finish, // just for the problem matcher to go inactive. if (!task.configurationProperties.isBackground) { - return this.executeTask(task, resolver, trigger, encounteredDependencies, alreadyResolved); + return this._executeTask(task, resolver, trigger, encounteredDependencies, alreadyResolved); } - const inactivePromise = this.createInactiveDependencyPromise(task); - return Promise.race([inactivePromise, this.executeTask(task, resolver, trigger, encounteredDependencies, alreadyResolved)]); + const inactivePromise = this._createInactiveDependencyPromise(task); + return Promise.race([inactivePromise, this._executeTask(task, resolver, trigger, encounteredDependencies, alreadyResolved)]); } - private async resolveAndFindExecutable(systemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, cwd: string | undefined, envPath: string | undefined): Promise<string> { - const command = await this.configurationResolverService.resolveAsync(workspaceFolder, CommandString.value(task.command.name!)); - cwd = cwd ? await this.configurationResolverService.resolveAsync(workspaceFolder, cwd) : undefined; - const paths = envPath ? await Promise.all(envPath.split(path.delimiter).map(p => this.configurationResolverService.resolveAsync(workspaceFolder, p))) : undefined; + private async _resolveAndFindExecutable(systemInfo: ITaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, cwd: string | undefined, envPath: string | undefined): Promise<string> { + const command = await this._configurationResolverService.resolveAsync(workspaceFolder, CommandString.value(task.command.name!)); + cwd = cwd ? await this._configurationResolverService.resolveAsync(workspaceFolder, cwd) : undefined; + const paths = envPath ? await Promise.all(envPath.split(path.delimiter).map(p => this._configurationResolverService.resolveAsync(workspaceFolder, p))) : undefined; let foundExecutable = await systemInfo?.findExecutable(command, cwd, paths); if (!foundExecutable) { foundExecutable = path.join(cwd ?? '', command); @@ -606,7 +625,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return foundExecutable; } - private findUnresolvedVariables(variables: Set<string>, alreadyResolved: Map<string, string>): Set<string> { + private _findUnresolvedVariables(variables: Set<string>, alreadyResolved: Map<string, string>): Set<string> { if (alreadyResolved.size === 0) { return variables; } @@ -619,7 +638,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return unresolved; } - private mergeMaps(mergeInto: Map<string, string>, mergeFrom: Map<string, string>) { + private _mergeMaps(mergeInto: Map<string, string>, mergeFrom: Map<string, string>) { for (const entry of mergeFrom) { if (!mergeInto.has(entry[0])) { mergeInto.set(entry[0], entry[1]); @@ -627,19 +646,19 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } - private async acquireInput(taskSystemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set<string>, alreadyResolved: Map<string, string>): Promise<ResolvedVariables | undefined> { - const resolved = await this.resolveVariablesFromSet(taskSystemInfo, workspaceFolder, task, variables, alreadyResolved); - this.fireTaskEvent(TaskEvent.create(TaskEventKind.AcquiredInput, task)); + private async _acquireInput(taskSystemInfo: ITaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set<string>, alreadyResolved: Map<string, string>): Promise<IResolvedVariables | undefined> { + const resolved = await this._resolveVariablesFromSet(taskSystemInfo, workspaceFolder, task, variables, alreadyResolved); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.AcquiredInput, task)); return resolved; } - private resolveVariablesFromSet(taskSystemInfo: TaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set<string>, alreadyResolved: Map<string, string>): Promise<ResolvedVariables | undefined> { - let isProcess = task.command && task.command.runtime === RuntimeType.Process; - let options = task.command && task.command.options ? task.command.options : undefined; - let cwd = options ? options.cwd : undefined; + private _resolveVariablesFromSet(taskSystemInfo: ITaskSystemInfo | undefined, workspaceFolder: IWorkspaceFolder | undefined, task: CustomTask | ContributedTask, variables: Set<string>, alreadyResolved: Map<string, string>): Promise<IResolvedVariables | undefined> { + const isProcess = task.command && task.command.runtime === RuntimeType.Process; + const options = task.command && task.command.options ? task.command.options : undefined; + const cwd = options ? options.cwd : undefined; let envPath: string | undefined = undefined; if (options && options.env) { - for (let key of Object.keys(options.env)) { + for (const key of Object.keys(options.env)) { if (key.toLowerCase() === 'path') { if (Types.isString(options.env[key])) { envPath = options.env[key]; @@ -648,10 +667,10 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } } - const unresolved = this.findUnresolvedVariables(variables, alreadyResolved); - let resolvedVariables: Promise<ResolvedVariables | undefined>; + const unresolved = this._findUnresolvedVariables(variables, alreadyResolved); + let resolvedVariables: Promise<IResolvedVariables | undefined>; if (taskSystemInfo && workspaceFolder) { - let resolveSet: ResolveSet = { + const resolveSet: IResolveSet = { variables: unresolved }; @@ -669,12 +688,12 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return undefined; } - this.mergeMaps(alreadyResolved, resolved.variables); + this._mergeMaps(alreadyResolved, resolved.variables); resolved.variables = new Map(alreadyResolved); if (isProcess) { let process = CommandString.value(task.command.name!); if (taskSystemInfo.platform === Platform.Platform.Windows) { - process = await this.resolveAndFindExecutable(taskSystemInfo, workspaceFolder, task, cwd, envPath); + process = await this._resolveAndFindExecutable(taskSystemInfo, workspaceFolder, task, cwd, envPath); } resolved.variables.set(TerminalTaskSystem.ProcessVarName, process); } @@ -682,24 +701,24 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }); return resolvedVariables; } else { - let variablesArray = new Array<string>(); + const variablesArray = new Array<string>(); unresolved.forEach(variable => variablesArray.push(variable)); - return new Promise<ResolvedVariables | undefined>((resolve, reject) => { - this.configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks', undefined, TaskSourceKind.toConfigurationTarget(task._source.kind)).then(async (resolvedVariablesMap: Map<string, string> | undefined) => { + return new Promise<IResolvedVariables | undefined>((resolve, reject) => { + this._configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks', undefined, TaskSourceKind.toConfigurationTarget(task._source.kind)).then(async (resolvedVariablesMap: Map<string, string> | undefined) => { if (resolvedVariablesMap) { - this.mergeMaps(alreadyResolved, resolvedVariablesMap); + this._mergeMaps(alreadyResolved, resolvedVariablesMap); resolvedVariablesMap = new Map(alreadyResolved); if (isProcess) { let processVarValue: string; if (Platform.isWindows) { - processVarValue = await this.resolveAndFindExecutable(taskSystemInfo, workspaceFolder, task, cwd, envPath); + processVarValue = await this._resolveAndFindExecutable(taskSystemInfo, workspaceFolder, task, cwd, envPath); } else { - processVarValue = await this.configurationResolverService.resolveAsync(workspaceFolder, CommandString.value(task.command.name!)); + processVarValue = await this._configurationResolverService.resolveAsync(workspaceFolder, CommandString.value(task.command.name!)); } resolvedVariablesMap.set(TerminalTaskSystem.ProcessVarName, processVarValue); } - let resolvedVariablesResult: ResolvedVariables = { + const resolvedVariablesResult: IResolvedVariables = { variables: resolvedVariablesMap, }; resolve(resolvedVariablesResult); @@ -713,28 +732,28 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } - private executeCommand(task: CustomTask | ContributedTask, trigger: string, alreadyResolved: Map<string, string>): Promise<ITaskSummary> { + private _executeCommand(task: CustomTask | ContributedTask, trigger: string, alreadyResolved: Map<string, string>): Promise<ITaskSummary> { const taskWorkspaceFolder = task.getWorkspaceFolder(); let workspaceFolder: IWorkspaceFolder | undefined; if (taskWorkspaceFolder) { - workspaceFolder = this.currentTask.workspaceFolder = taskWorkspaceFolder; + workspaceFolder = this._currentTask.workspaceFolder = taskWorkspaceFolder; } else { - const folders = this.contextService.getWorkspace().folders; + const folders = this._contextService.getWorkspace().folders; workspaceFolder = folders.length > 0 ? folders[0] : undefined; } - const systemInfo: TaskSystemInfo | undefined = this.currentTask.systemInfo = this.taskSystemInfoResolver(workspaceFolder); + const systemInfo: ITaskSystemInfo | undefined = this._currentTask.systemInfo = this._taskSystemInfoResolver(workspaceFolder); - let variables = new Set<string>(); - this.collectTaskVariables(variables, task); - const resolvedVariables = this.acquireInput(systemInfo, workspaceFolder, task, variables, alreadyResolved); + const variables = new Set<string>(); + this._collectTaskVariables(variables, task); + const resolvedVariables = this._acquireInput(systemInfo, workspaceFolder, task, variables, alreadyResolved); return resolvedVariables.then((resolvedVariables) => { - if (resolvedVariables && !this.isTaskEmpty(task)) { - this.currentTask.resolvedVariables = resolvedVariables; - return this.executeInTerminal(task, trigger, new VariableResolver(workspaceFolder, systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder); + if (resolvedVariables && !this._isTaskEmpty(task)) { + this._currentTask.resolvedVariables = resolvedVariables; + return this._executeInTerminal(task, trigger, new VariableResolver(workspaceFolder, systemInfo, resolvedVariables.variables, this._configurationResolverService), workspaceFolder); } else { // Allows the taskExecutions array to be updated in the extension host - this.fireTaskEvent(TaskEvent.create(TaskEventKind.End, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.End, task)); return Promise.resolve({ exitCode: 0 }); } }, reason => { @@ -742,19 +761,19 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }); } - private isTaskEmpty(task: CustomTask | ContributedTask): boolean { + private _isTaskEmpty(task: CustomTask | ContributedTask): boolean { const isCustomExecution = (task.command.runtime === RuntimeType.CustomExecution); return !((task.command !== undefined) && task.command.runtime && (isCustomExecution || (task.command.name !== undefined))); } - private reexecuteCommand(task: CustomTask | ContributedTask, trigger: string, alreadyResolved: Map<string, string>): Promise<ITaskSummary> { - const lastTask = this.lastTask; + private _reexecuteCommand(task: CustomTask | ContributedTask, trigger: string, alreadyResolved: Map<string, string>): Promise<ITaskSummary> { + const lastTask = this._lastTask; if (!lastTask) { return Promise.reject(new Error('No task previously run')); } - const workspaceFolder = this.currentTask.workspaceFolder = lastTask.workspaceFolder; - let variables = new Set<string>(); - this.collectTaskVariables(variables, task); + const workspaceFolder = this._currentTask.workspaceFolder = lastTask.workspaceFolder; + const variables = new Set<string>(); + this._collectTaskVariables(variables, task); // Check that the task hasn't changed to include new variables let hasAllVariables = true; @@ -765,33 +784,33 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }); if (!hasAllVariables) { - return this.acquireInput(lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().workspaceFolder, task, variables, alreadyResolved).then((resolvedVariables) => { + return this._acquireInput(lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().workspaceFolder, task, variables, alreadyResolved).then((resolvedVariables) => { if (!resolvedVariables) { // Allows the taskExecutions array to be updated in the extension host - this.fireTaskEvent(TaskEvent.create(TaskEventKind.End, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.End, task)); return { exitCode: 0 }; } - this.currentTask.resolvedVariables = resolvedVariables; - return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this.configurationResolverService), workspaceFolder!); + this._currentTask.resolvedVariables = resolvedVariables; + return this._executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, resolvedVariables.variables, this._configurationResolverService), workspaceFolder!); }, reason => { return Promise.reject(reason); }); } else { - this.currentTask.resolvedVariables = lastTask.getVerifiedTask().resolvedVariables; - return this.executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().resolvedVariables.variables, this.configurationResolverService), workspaceFolder!); + this._currentTask.resolvedVariables = lastTask.getVerifiedTask().resolvedVariables; + return this._executeInTerminal(task, trigger, new VariableResolver(lastTask.getVerifiedTask().workspaceFolder, lastTask.getVerifiedTask().systemInfo, lastTask.getVerifiedTask().resolvedVariables.variables, this._configurationResolverService), workspaceFolder!); } } - private async executeInTerminal(task: CustomTask | ContributedTask, trigger: string, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<ITaskSummary> { + private async _executeInTerminal(task: CustomTask | ContributedTask, trigger: string, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<ITaskSummary> { let terminal: ITerminalInstance | undefined = undefined; let error: TaskError | undefined = undefined; let promise: Promise<ITaskSummary> | undefined = undefined; if (task.configurationProperties.isBackground) { - const problemMatchers = await this.resolveMatchers(resolver, task.configurationProperties.problemMatchers); - let watchingProblemMatcher = new WatchingProblemCollector(problemMatchers, this.markerService, this.modelService, this.fileService); + const problemMatchers = await this._resolveMatchers(resolver, task.configurationProperties.problemMatchers); + const watchingProblemMatcher = new WatchingProblemCollector(problemMatchers, this._markerService, this._modelService, this._fileService); if ((problemMatchers.length > 0) && !watchingProblemMatcher.isWatching()) { - this.appendOutput(nls.localize('TerminalTaskSystem.nonWatchingMatcher', 'Task {0} is a background task but uses a problem matcher without a background pattern', task._label)); - this.showOutput(); + this._appendOutput(nls.localize('TerminalTaskSystem.nonWatchingMatcher', 'Task {0} is a background task but uses a problem matcher without a background pattern', task._label)); + this._showOutput(); } const toDispose = new DisposableStore(); let eventCounter: number = 0; @@ -799,24 +818,24 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { toDispose.add(watchingProblemMatcher.onDidStateChange((event) => { if (event.kind === ProblemCollectorEventKind.BackgroundProcessingBegins) { eventCounter++; - this.busyTasks[mapKey] = task; - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Active, task)); + this._busyTasks[mapKey] = task; + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Active, task)); } else if (event.kind === ProblemCollectorEventKind.BackgroundProcessingEnds) { eventCounter--; - if (this.busyTasks[mapKey]) { - delete this.busyTasks[mapKey]; + if (this._busyTasks[mapKey]) { + delete this._busyTasks[mapKey]; } - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Inactive, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Inactive, task)); if (eventCounter === 0) { if ((watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && (watchingProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error)) { - let reveal = task.command.presentation!.reveal; - let revealProblems = task.command.presentation!.revealProblems; + const reveal = task.command.presentation!.reveal; + const revealProblems = task.command.presentation!.revealProblems; if (revealProblems === RevealProblemKind.OnProblem) { - this.viewsService.openView(Constants.MARKERS_VIEW_ID, true); + this._viewsService.openView(Markers.MARKERS_VIEW_ID, true); } else if (reveal === RevealKind.Silent) { - this.terminalService.setActiveInstance(terminal!); - this.terminalGroupService.showPanel(false); + this._terminalService.setActiveInstance(terminal!); + this._terminalGroupService.showPanel(false); } } } @@ -824,7 +843,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { })); watchingProblemMatcher.aboutToStart(); let delayer: Async.Delayer<any> | undefined = undefined; - [terminal, error] = await this.createTerminal(task, resolver, workspaceFolder); + [terminal, error] = await this._createTerminal(task, resolver, workspaceFolder); if (error) { return Promise.reject(new Error((<TaskError>error).message)); @@ -832,18 +851,18 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (!terminal) { return Promise.reject(new Error(`Failed to create terminal for task ${task._label}`)); } - this.terminalStatusManager.addTerminal(task, terminal, watchingProblemMatcher); + this._terminalStatusManager.addTerminal(task, terminal, watchingProblemMatcher); let processStartedSignaled = false; terminal.processReady.then(() => { if (!processStartedSignaled) { - this.fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); processStartedSignaled = true; } }, (_error) => { - this.logService.error('Task terminal process never got ready'); + this._logService.error('Task terminal process never got ready'); }); - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Start, task, terminal.instanceId)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Start, task, terminal.instanceId)); const onData = terminal.onLineData((line) => { watchingProblemMatcher.processLine(line); if (!delayer) { @@ -859,29 +878,29 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const exitCode = typeof terminalLaunchResult === 'number' ? terminalLaunchResult : terminalLaunchResult?.code; onData.dispose(); onExit.dispose(); - let key = task.getMapKey(); - if (this.busyTasks[mapKey]) { - delete this.busyTasks[mapKey]; + const key = task.getMapKey(); + if (this._busyTasks[mapKey]) { + delete this._busyTasks[mapKey]; } - this.removeFromActiveTasks(task); - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Changed)); + this._removeFromActiveTasks(task); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Changed)); if (terminalLaunchResult !== undefined) { // Only keep a reference to the terminal if it is not being disposed. switch (task.command.presentation!.panel) { case PanelKind.Dedicated: - this.sameTaskTerminals[key] = terminal!.instanceId.toString(); + this._sameTaskTerminals[key] = terminal!.instanceId.toString(); break; case PanelKind.Shared: - this.idleTaskTerminals.set(key, terminal!.instanceId.toString(), Touch.AsOld); + this._idleTaskTerminals.set(key, terminal!.instanceId.toString(), Touch.AsOld); break; } } - let reveal = task.command.presentation!.reveal; + const reveal = task.command.presentation!.reveal; if ((reveal === RevealKind.Silent) && ((exitCode !== 0) || (watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && (watchingProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error))) { try { - this.terminalService.setActiveInstance(terminal!); - this.terminalGroupService.showPanel(false); + this._terminalService.setActiveInstance(terminal!); + this._terminalGroupService.showPanel(false); } catch (e) { // If the terminal has already been disposed, then setting the active instance will fail. #99828 // There is nothing else to do here. @@ -890,23 +909,23 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { watchingProblemMatcher.done(); watchingProblemMatcher.dispose(); if (!processStartedSignaled) { - this.fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); processStartedSignaled = true; } - this.fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode ?? undefined)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode ?? undefined)); for (let i = 0; i < eventCounter; i++) { - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Inactive, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Inactive, task)); } eventCounter = 0; - this.fireTaskEvent(TaskEvent.create(TaskEventKind.End, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.End, task)); toDispose.dispose(); resolve({ exitCode: exitCode ?? undefined }); }); }); } else { - [terminal, error] = await this.createTerminal(task, resolver, workspaceFolder); + [terminal, error] = await this._createTerminal(task, resolver, workspaceFolder); if (error) { return Promise.reject(new Error((<TaskError>error).message)); @@ -918,19 +937,19 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { let processStartedSignaled = false; terminal.processReady.then(() => { if (!processStartedSignaled) { - this.fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal!.processId!)); processStartedSignaled = true; } }, (_error) => { // The process never got ready. Need to think how to handle this. }); - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Start, task, terminal.instanceId, resolver.values)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Start, task, terminal.instanceId, resolver.values)); const mapKey = task.getMapKey(); - this.busyTasks[mapKey] = task; - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Active, task)); - let problemMatchers = await this.resolveMatchers(resolver, task.configurationProperties.problemMatchers); - let startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this.markerService, this.modelService, ProblemHandlingStrategy.Clean, this.fileService); - this.terminalStatusManager.addTerminal(task, terminal, startStopProblemMatcher); + this._busyTasks[mapKey] = task; + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Active, task)); + const problemMatchers = await this._resolveMatchers(resolver, task.configurationProperties.problemMatchers); + const startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this._markerService, this._modelService, ProblemHandlingStrategy.Clean, this._fileService); + this._terminalStatusManager.addTerminal(task, terminal, startStopProblemMatcher); const onData = terminal.onLineData((line) => { startStopProblemMatcher.processLine(line); }); @@ -938,30 +957,30 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const onExit = terminal!.onExit((terminalLaunchResult) => { const exitCode = typeof terminalLaunchResult === 'number' ? terminalLaunchResult : terminalLaunchResult?.code; onExit.dispose(); - let key = task.getMapKey(); - this.removeFromActiveTasks(task); - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Changed)); + const key = task.getMapKey(); + this._removeFromActiveTasks(task); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Changed)); if (terminalLaunchResult !== undefined) { // Only keep a reference to the terminal if it is not being disposed. switch (task.command.presentation!.panel) { case PanelKind.Dedicated: - this.sameTaskTerminals[key] = terminal!.instanceId.toString(); + this._sameTaskTerminals[key] = terminal!.instanceId.toString(); break; case PanelKind.Shared: - this.idleTaskTerminals.set(key, terminal!.instanceId.toString(), Touch.AsOld); + this._idleTaskTerminals.set(key, terminal!.instanceId.toString(), Touch.AsOld); break; } } - let reveal = task.command.presentation!.reveal; - let revealProblems = task.command.presentation!.revealProblems; - let revealProblemPanel = terminal && (revealProblems === RevealProblemKind.OnProblem) && (startStopProblemMatcher.numberOfMatches > 0); + const reveal = task.command.presentation!.reveal; + const revealProblems = task.command.presentation!.revealProblems; + const revealProblemPanel = terminal && (revealProblems === RevealProblemKind.OnProblem) && (startStopProblemMatcher.numberOfMatches > 0); if (revealProblemPanel) { - this.viewsService.openView(Constants.MARKERS_VIEW_ID); + this._viewsService.openView(Markers.MARKERS_VIEW_ID); } else if (terminal && (reveal === RevealKind.Silent) && ((exitCode !== 0) || (startStopProblemMatcher.numberOfMatches > 0) && startStopProblemMatcher.maxMarkerSeverity && (startStopProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error))) { try { - this.terminalService.setActiveInstance(terminal); - this.terminalGroupService.showPanel(false); + this._terminalService.setActiveInstance(terminal); + this._terminalGroupService.showPanel(false); } catch (e) { // If the terminal has already been disposed, then setting the active instance will fail. #99828 // There is nothing else to do here. @@ -974,45 +993,45 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { startStopProblemMatcher.dispose(); }, 100); if (!processStartedSignaled && terminal) { - this.fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId!)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessStarted, task, terminal.processId!)); processStartedSignaled = true; } - this.fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode ?? undefined)); - if (this.busyTasks[mapKey]) { - delete this.busyTasks[mapKey]; + this._fireTaskEvent(TaskEvent.create(TaskEventKind.ProcessEnded, task, exitCode ?? undefined)); + if (this._busyTasks[mapKey]) { + delete this._busyTasks[mapKey]; } - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Inactive, task)); - this.fireTaskEvent(TaskEvent.create(TaskEventKind.End, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Inactive, task)); + this._fireTaskEvent(TaskEvent.create(TaskEventKind.End, task)); resolve({ exitCode: exitCode ?? undefined }); }); }); } - let showProblemPanel = task.command.presentation && (task.command.presentation.revealProblems === RevealProblemKind.Always); + const showProblemPanel = task.command.presentation && (task.command.presentation.revealProblems === RevealProblemKind.Always); if (showProblemPanel) { - this.viewsService.openView(Constants.MARKERS_VIEW_ID); + this._viewsService.openView(Markers.MARKERS_VIEW_ID); } else if (task.command.presentation && (task.command.presentation.reveal === RevealKind.Always)) { - this.terminalService.setActiveInstance(terminal); - this.terminalGroupService.showPanel(task.command.presentation.focus); + this._terminalService.setActiveInstance(terminal); + this._terminalGroupService.showPanel(task.command.presentation.focus); } - this.activeTasks[task.getMapKey()] = { terminal, task, promise }; - this.fireTaskEvent(TaskEvent.create(TaskEventKind.Changed)); + this._activeTasks[task.getMapKey()] = { terminal, task, promise }; + this._fireTaskEvent(TaskEvent.create(TaskEventKind.Changed)); return promise; } - private createTerminalName(task: CustomTask | ContributedTask): string { - const needsFolderQualification = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; + private _createTerminalName(task: CustomTask | ContributedTask): string { + const needsFolderQualification = this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; return needsFolderQualification ? task.getQualifiedLabel() : (task.configurationProperties.name || ''); } - private async createShellLaunchConfig(task: CustomTask | ContributedTask, workspaceFolder: IWorkspaceFolder | undefined, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: boolean | string): Promise<IShellLaunchConfig | undefined> { + private async _createShellLaunchConfig(task: CustomTask | ContributedTask, workspaceFolder: IWorkspaceFolder | undefined, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: boolean | string | ((exitCode: number) => string)): Promise<IShellLaunchConfig | undefined> { let shellLaunchConfig: IShellLaunchConfig; - let isShellCommand = task.command.runtime === RuntimeType.Shell; - let needsFolderQualification = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; - let terminalName = this.createTerminalName(task); + const isShellCommand = task.command.runtime === RuntimeType.Shell; + const needsFolderQualification = this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; + const terminalName = this._createTerminalName(task); const type = 'Task'; - let originalCommand = task.command.name; + const originalCommand = task.command.name; if (isShellCommand) { let os: Platform.OperatingSystem; switch (platform) { @@ -1021,48 +1040,56 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { case Platform.Platform.Linux: default: os = Platform.OperatingSystem.Linux; break; } - const defaultProfile = await this.terminalProfileResolverService.getDefaultProfile({ + const defaultProfile = await this._terminalProfileResolverService.getDefaultProfile({ allowAutomationShell: true, os, - remoteAuthority: this.environmentService.remoteAuthority + remoteAuthority: this._environmentService.remoteAuthority }); + let icon: URI | ThemeIcon | { light: URI; dark: URI } | undefined; + if (task.configurationProperties.icon?.id) { + icon = ThemeIcon.fromId(task.configurationProperties.icon.id); + } else { + const taskGroupKind = task.configurationProperties.group ? GroupKind.to(task.configurationProperties.group) : undefined; + const kindId = typeof taskGroupKind === 'string' ? taskGroupKind : taskGroupKind?.kind; + icon = kindId === 'test' ? ThemeIcon.fromId(Codicon.beaker.id) : defaultProfile.icon; + } shellLaunchConfig = { name: terminalName, type, executable: defaultProfile.path, args: defaultProfile.args, - icon: defaultProfile.icon, env: { ...defaultProfile.env }, - color: defaultProfile.color, + icon, + color: task.configurationProperties.icon?.color || undefined, waitOnExit }; let shellSpecified: boolean = false; - let shellOptions: ShellConfiguration | undefined = task.command.options && task.command.options.shell; + const shellOptions: IShellConfiguration | undefined = task.command.options && task.command.options.shell; if (shellOptions) { if (shellOptions.executable) { // Clear out the args so that we don't end up with mismatched args. if (shellOptions.executable !== shellLaunchConfig.executable) { shellLaunchConfig.args = undefined; } - shellLaunchConfig.executable = await this.resolveVariable(variableResolver, shellOptions.executable); + shellLaunchConfig.executable = await this._resolveVariable(variableResolver, shellOptions.executable); shellSpecified = true; } if (shellOptions.args) { - shellLaunchConfig.args = await this.resolveVariables(variableResolver, shellOptions.args.slice()); + shellLaunchConfig.args = await this._resolveVariables(variableResolver, shellOptions.args.slice()); } } if (shellLaunchConfig.args === undefined) { shellLaunchConfig.args = []; } - let shellArgs = Array.isArray(shellLaunchConfig.args!) ? <string[]>shellLaunchConfig.args!.slice(0) : [shellLaunchConfig.args!]; - let toAdd: string[] = []; - let basename = path.posix.basename((await this.pathService.fileURI(shellLaunchConfig.executable!)).path).toLowerCase(); - let commandLine = this.buildShellCommandLine(platform, basename, shellOptions, command, originalCommand, args); + const shellArgs = Array.isArray(shellLaunchConfig.args!) ? <string[]>shellLaunchConfig.args!.slice(0) : [shellLaunchConfig.args!]; + const toAdd: string[] = []; + const basename = path.posix.basename((await this._pathService.fileURI(shellLaunchConfig.executable!)).path).toLowerCase(); + const commandLine = this._buildShellCommandLine(platform, basename, shellOptions, command, originalCommand, args); let windowsShellArgs: boolean = false; if (platform === Platform.Platform.Windows) { windowsShellArgs = true; // If we don't have a cwd, then the terminal uses the home dir. - const userHome = await this.pathService.userHome(); + const userHome = await this._pathService.userHome(); if (basename === 'cmd.exe' && ((options.cwd && isUNC(options.cwd)) || (!options.cwd && isUNC(userHome.fsPath)))) { return undefined; } @@ -1089,13 +1116,13 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { // Under Mac remove -l to not start it as a login shell. if (platform === Platform.Platform.Mac) { // Background on -l on osx https://github.com/microsoft/vscode/issues/107563 - const osxShellArgs = this.configurationService.inspect(TerminalSettingId.ShellArgsMacOs); + const osxShellArgs = this._configurationService.inspect(TerminalSettingId.ShellArgsMacOs); if ((osxShellArgs.user === undefined) && (osxShellArgs.userLocal === undefined) && (osxShellArgs.userLocalValue === undefined) && (osxShellArgs.userRemote === undefined) && (osxShellArgs.userRemoteValue === undefined) && (osxShellArgs.userValue === undefined) && (osxShellArgs.workspace === undefined) && (osxShellArgs.workspaceFolder === undefined) && (osxShellArgs.workspaceFolderValue === undefined) && (osxShellArgs.workspaceValue === undefined)) { - let index = shellArgs.indexOf('-l'); + const index = shellArgs.indexOf('-l'); if (index !== -1) { shellArgs.splice(index, 1); } @@ -1104,32 +1131,42 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { toAdd.push('-c'); } } - const combinedShellArgs = this.addAllArgument(toAdd, shellArgs); + const combinedShellArgs = this._addAllArgument(toAdd, shellArgs); combinedShellArgs.push(commandLine); shellLaunchConfig.args = windowsShellArgs ? combinedShellArgs.join(' ') : combinedShellArgs; if (task.command.presentation && task.command.presentation.echo) { if (needsFolderQualification && workspaceFolder) { - shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${workspaceFolder.name}: ${commandLine} <\x1b[0m\n`; + shellLaunchConfig.initialText = this.taskShellIntegrationStartSequence + formatMessageForTerminal(nls.localize({ + key: 'task.executingInFolder', + comment: ['The workspace folder the task is running in', 'The task command line or label'] + }, 'Executing task in folder {0}: {1}', workspaceFolder.name, commandLine), { excludeLeadingNewLine: true }) + this.taskShellIntegrationOutputSequence; } else { - shellLaunchConfig.initialText = `\x1b[1m> Executing task: ${commandLine} <\x1b[0m\n`; + shellLaunchConfig.initialText = this.taskShellIntegrationStartSequence + formatMessageForTerminal(nls.localize({ + key: 'task.executing', + comment: ['The task command line or label'] + }, 'Executing task: {0}', commandLine), { excludeLeadingNewLine: true }) + this.taskShellIntegrationOutputSequence; } + } else { + shellLaunchConfig.initialText = this.taskShellIntegrationStartSequence + this.taskShellIntegrationOutputSequence; } } else { - let commandExecutable = (task.command.runtime !== RuntimeType.CustomExecution) ? CommandString.value(command) : undefined; - let executable = !isShellCommand - ? await this.resolveVariable(variableResolver, await this.resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}')) + const commandExecutable = (task.command.runtime !== RuntimeType.CustomExecution) ? CommandString.value(command) : undefined; + const executable = !isShellCommand + ? await this._resolveVariable(variableResolver, await this._resolveVariable(variableResolver, '${' + TerminalTaskSystem.ProcessVarName + '}')) : commandExecutable; // When we have a process task there is no need to quote arguments. So we go ahead and take the string value. shellLaunchConfig = { name: terminalName, type, + icon: task.configurationProperties.icon?.id ? ThemeIcon.fromId(task.configurationProperties.icon.id) : undefined, + color: task.configurationProperties.icon?.color || undefined, executable: executable, args: args.map(a => Types.isString(a) ? a : a.value), waitOnExit }; if (task.command.presentation && task.command.presentation.echo) { - let getArgsToEcho = (args: string | string[] | undefined): string => { + const getArgsToEcho = (args: string | string[] | undefined): string => { if (!args || args.length === 0) { return ''; } @@ -1139,10 +1176,18 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return args.join(' '); }; if (needsFolderQualification && workspaceFolder) { - shellLaunchConfig.initialText = `\x1b[1m> Executing task in folder ${workspaceFolder.name}: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`; + shellLaunchConfig.initialText = this.taskShellIntegrationStartSequence + formatMessageForTerminal(nls.localize({ + key: 'task.executingInFolder', + comment: ['The workspace folder the task is running in', 'The task command line or label'] + }, 'Executing task in folder {0}: {1}', workspaceFolder.name, `${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)}`), { excludeLeadingNewLine: true }) + this.taskShellIntegrationOutputSequence; } else { - shellLaunchConfig.initialText = `\x1b[1m> Executing task: ${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)} <\x1b[0m\n`; + shellLaunchConfig.initialText = this.taskShellIntegrationStartSequence + formatMessageForTerminal(nls.localize({ + key: 'task.executing', + comment: ['The task command line or label'] + }, 'Executing task: {0}', `${shellLaunchConfig.executable} ${getArgsToEcho(shellLaunchConfig.args)}`), { excludeLeadingNewLine: true }) + this.taskShellIntegrationOutputSequence; } + } else { + shellLaunchConfig.initialText = this.taskShellIntegrationStartSequence + this.taskShellIntegrationOutputSequence; } } @@ -1154,7 +1199,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } // This must be normalized to the OS - shellLaunchConfig.cwd = isUNC(cwd) ? cwd : resources.toLocalResource(URI.from({ scheme: Schemas.file, path: cwd }), this.environmentService.remoteAuthority, this.pathService.defaultUriScheme); + shellLaunchConfig.cwd = isUNC(cwd) ? cwd : resources.toLocalResource(URI.from({ scheme: Schemas.file, path: cwd }), this._environmentService.remoteAuthority, this._pathService.defaultUriScheme); } if (options.env) { if (shellLaunchConfig.env) { @@ -1168,7 +1213,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return shellLaunchConfig; } - private addAllArgument(shellCommandArgs: string[], configuredShellArgs: string[]): string[] { + private _addAllArgument(shellCommandArgs: string[], configuredShellArgs: string[]): string[] { const combinedShellArgs: string[] = Objects.deepClone(configuredShellArgs); shellCommandArgs.forEach(element => { const shouldAddShellCommandArg = configuredShellArgs.every((arg, index) => { @@ -1186,34 +1231,34 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return combinedShellArgs; } - private async doCreateTerminal(group: string | undefined, launchConfigs: IShellLaunchConfig): Promise<ITerminalInstance> { + private async _doCreateTerminal(group: string | undefined, launchConfigs: IShellLaunchConfig): Promise<ITerminalInstance> { if (group) { // Try to find an existing terminal to split. // Even if an existing terminal is found, the split can fail if the terminal width is too small. - for (const terminal of values(this.terminals)) { + for (const terminal of Object.values(this._terminals)) { if (terminal.group === group) { - this.logService.trace(`Found terminal to split for group ${group}`); + this._logService.trace(`Found terminal to split for group ${group}`); const originalInstance = terminal.terminal; - const result = await this.terminalService.createTerminal({ location: { parentTerminal: originalInstance }, config: launchConfigs }); + const result = await this._terminalService.createTerminal({ location: { parentTerminal: originalInstance }, config: launchConfigs }); if (result) { return result; } } } - this.logService.trace(`No terminal found to split for group ${group}`); + this._logService.trace(`No terminal found to split for group ${group}`); } // Either no group is used, no terminal with the group exists or splitting an existing terminal failed. - const createdTerminal = await this.terminalService.createTerminal({ location: TerminalLocation.Panel, config: launchConfigs }); - this.logService.trace('Created a new task terminal'); + const createdTerminal = await this._terminalService.createTerminal({ location: TerminalLocation.Panel, config: launchConfigs }); + this._logService.trace('Created a new task terminal'); return createdTerminal; } - private async createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<[ITerminalInstance | undefined, TaskError | undefined]> { - let platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform; - let options = await this.resolveOptions(resolver, task.command.options); + private async _createTerminal(task: CustomTask | ContributedTask, resolver: VariableResolver, workspaceFolder: IWorkspaceFolder | undefined): Promise<[ITerminalInstance | undefined, TaskError | undefined]> { + const platform = resolver.taskSystemInfo ? resolver.taskSystemInfo.platform : Platform.platform; + const options = await this._resolveOptions(resolver, task.command.options); const presentationOptions = task.command.presentation; - let waitOnExit: boolean | string = false; + let waitOnExit: boolean | string | ((exitCode: number) => string) = false; if (!presentationOptions) { throw new Error('Task presentation options should not be undefined here.'); } @@ -1221,9 +1266,9 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if ((presentationOptions.close === undefined) || (presentationOptions.close === false)) { if ((presentationOptions.reveal !== RevealKind.Never) || !task.configurationProperties.isBackground || (presentationOptions.close === false)) { if (presentationOptions.panel === PanelKind.New) { - waitOnExit = nls.localize('closeTerminal', 'Press any key to close the terminal.'); + waitOnExit = taskShellIntegrationWaitOnExitSequence(nls.localize('closeTerminal', 'Press any key to close the terminal.')); } else if (presentationOptions.showReuseMessage) { - waitOnExit = nls.localize('reuseTerminal', 'Terminal will be reused by tasks, press any key to close it.'); + waitOnExit = taskShellIntegrationWaitOnExitSequence(nls.localize('reuseTerminal', 'Terminal will be reused by tasks, press any key to close it.')); } else { waitOnExit = true; } @@ -1237,53 +1282,58 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { let launchConfigs: IShellLaunchConfig | undefined; if (task.command.runtime === RuntimeType.CustomExecution) { - this.currentTask.shellLaunchConfig = launchConfigs = { - customPtyImplementation: (id, cols, rows) => new TerminalProcessExtHostProxy(id, cols, rows, this.terminalService), + this._currentTask.shellLaunchConfig = launchConfigs = { + customPtyImplementation: (id, cols, rows) => new TerminalProcessExtHostProxy(id, cols, rows, this._terminalService), waitOnExit, - name: this.createTerminalName(task), - initialText: task.command.presentation && task.command.presentation.echo ? `\x1b[1m> Executing task: ${task._label} <\x1b[0m\n` : undefined, - isFeatureTerminal: true + name: this._createTerminalName(task), + initialText: task.command.presentation && task.command.presentation.echo ? formatMessageForTerminal(nls.localize({ + key: 'task.executing', + comment: ['The task command line or label'] + }, 'Executing task: {0}', task._label), { excludeLeadingNewLine: true }) : undefined, + isFeatureTerminal: true, + icon: task.configurationProperties.icon?.id ? ThemeIcon.fromId(task.configurationProperties.icon.id) : undefined, + color: task.configurationProperties.icon?.color || undefined, }; } else { - let resolvedResult: { command: CommandString; args: CommandString[] } = await this.resolveCommandAndArgs(resolver, task.command); + const resolvedResult: { command: CommandString; args: CommandString[] } = await this._resolveCommandAndArgs(resolver, task.command); command = resolvedResult.command; args = resolvedResult.args; - this.currentTask.shellLaunchConfig = launchConfigs = await this.createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit); + this._currentTask.shellLaunchConfig = launchConfigs = await this._createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit); if (launchConfigs === undefined) { return [undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; } } - let prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated; - let allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared; - let group = presentationOptions.group; + const prefersSameTerminal = presentationOptions.panel === PanelKind.Dedicated; + const allowsSharedTerminal = presentationOptions.panel === PanelKind.Shared; + const group = presentationOptions.group; - let taskKey = task.getMapKey(); - let terminalToReuse: TerminalData | undefined; + const taskKey = task.getMapKey(); + let terminalToReuse: ITerminalData | undefined; if (prefersSameTerminal) { - let terminalId = this.sameTaskTerminals[taskKey]; + const terminalId = this._sameTaskTerminals[taskKey]; if (terminalId) { - terminalToReuse = this.terminals[terminalId]; - delete this.sameTaskTerminals[taskKey]; + terminalToReuse = this._terminals[terminalId]; + delete this._sameTaskTerminals[taskKey]; } } else if (allowsSharedTerminal) { // Always allow to reuse the terminal previously used by the same task. - let terminalId = this.idleTaskTerminals.remove(taskKey); + let terminalId = this._idleTaskTerminals.remove(taskKey); if (!terminalId) { // There is no idle terminal which was used by the same task. // Search for any idle terminal used previously by a task of the same group // (or, if the task has no group, a terminal used by a task without group). - for (const taskId of this.idleTaskTerminals.keys()) { - const idleTerminalId = this.idleTaskTerminals.get(taskId)!; - if (idleTerminalId && this.terminals[idleTerminalId] && this.terminals[idleTerminalId].group === group) { - terminalId = this.idleTaskTerminals.remove(taskId); + for (const taskId of this._idleTaskTerminals.keys()) { + const idleTerminalId = this._idleTaskTerminals.get(taskId)!; + if (idleTerminalId && this._terminals[idleTerminalId] && this._terminals[idleTerminalId].group === group) { + terminalId = this._idleTaskTerminals.remove(taskId); break; } } } if (terminalId) { - terminalToReuse = this.terminals[terminalId]; + terminalToReuse = this._terminals[terminalId]; } } if (terminalToReuse) { @@ -1297,42 +1347,42 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (task.command.presentation && task.command.presentation.clear) { terminalToReuse.terminal.clearBuffer(); } - this.terminals[terminalToReuse.terminal.instanceId.toString()].lastTask = taskKey; + this._terminals[terminalToReuse.terminal.instanceId.toString()].lastTask = taskKey; return [terminalToReuse.terminal, undefined]; } - this.terminalCreationQueue = this.terminalCreationQueue.then(() => this.doCreateTerminal(group, launchConfigs!)); - const result: ITerminalInstance = (await this.terminalCreationQueue)!; + this._terminalCreationQueue = this._terminalCreationQueue.then(() => this._doCreateTerminal(group, launchConfigs!)); + const result: ITerminalInstance = (await this._terminalCreationQueue)!; const terminalKey = result.instanceId.toString(); result.onDisposed((terminal) => { - let terminalData = this.terminals[terminalKey]; + const terminalData = this._terminals[terminalKey]; if (terminalData) { - delete this.terminals[terminalKey]; - delete this.sameTaskTerminals[terminalData.lastTask]; - this.idleTaskTerminals.delete(terminalData.lastTask); + delete this._terminals[terminalKey]; + delete this._sameTaskTerminals[terminalData.lastTask]; + this._idleTaskTerminals.delete(terminalData.lastTask); // Delete the task now as a work around for cases when the onExit isn't fired. // This can happen if the terminal wasn't shutdown with an "immediate" flag and is expected. // For correct terminal re-use, the task needs to be deleted immediately. // Note that this shouldn't be a problem anymore since user initiated terminal kills are now immediate. const mapKey = task.getMapKey(); - this.removeFromActiveTasks(task); - if (this.busyTasks[mapKey]) { - delete this.busyTasks[mapKey]; + this._removeFromActiveTasks(task); + if (this._busyTasks[mapKey]) { + delete this._busyTasks[mapKey]; } } }); - this.terminals[terminalKey] = { terminal: result, lastTask: taskKey, group }; + this._terminals[terminalKey] = { terminal: result, lastTask: taskKey, group }; return [result, undefined]; } - private buildShellCommandLine(platform: Platform.Platform, shellExecutable: string, shellOptions: ShellConfiguration | undefined, command: CommandString, originalCommand: CommandString | undefined, args: CommandString[]): string { - let basename = path.parse(shellExecutable).name.toLowerCase(); - let shellQuoteOptions = this.getQuotingOptions(basename, shellOptions, platform); + private _buildShellCommandLine(platform: Platform.Platform, shellExecutable: string, shellOptions: IShellConfiguration | undefined, command: CommandString, originalCommand: CommandString | undefined, args: CommandString[]): string { + const basename = path.parse(shellExecutable).name.toLowerCase(); + const shellQuoteOptions = this._getQuotingOptions(basename, shellOptions, platform); function needsQuotes(value: string): boolean { if (value.length >= 2) { - let first = value[0] === shellQuoteOptions.strong ? shellQuoteOptions.strong : value[0] === shellQuoteOptions.weak ? shellQuoteOptions.weak : undefined; + const first = value[0] === shellQuoteOptions.strong ? shellQuoteOptions.strong : value[0] === shellQuoteOptions.weak ? shellQuoteOptions.weak : undefined; if (first === value[value.length - 1]) { return false; } @@ -1340,7 +1390,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { let quote: string | undefined; for (let i = 0; i < value.length; i++) { // We found the end quote. - let ch = value[i]; + const ch = value[i]; if (ch === quote) { quote = undefined; } else if (quote !== undefined) { @@ -1367,12 +1417,12 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (Types.isString(shellQuoteOptions.escape)) { return [value.replace(/ /g, shellQuoteOptions.escape + ' '), true]; } else { - let buffer: string[] = []; - for (let ch of shellQuoteOptions.escape.charsToEscape) { + const buffer: string[] = []; + for (const ch of shellQuoteOptions.escape.charsToEscape) { buffer.push(`\\${ch}`); } - let regexp: RegExp = new RegExp('[' + buffer.join(',') + ']', 'g'); - let escapeChar = shellQuoteOptions.escape.escapeChar; + const regexp: RegExp = new RegExp('[' + buffer.join(',') + ']', 'g'); + const escapeChar = shellQuoteOptions.escape.escapeChar; return [value.replace(regexp, (match) => escapeChar + match), true]; } } @@ -1398,7 +1448,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return command; } - let result: string[] = []; + const result: string[] = []; let commandQuoted = false; let argQuoted = false; let value: string; @@ -1406,7 +1456,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { [value, quoted] = quoteIfNecessary(command); result.push(value); commandQuoted = quoted; - for (let arg of args) { + for (const arg of args) { [value, quoted] = quoteIfNecessary(arg); result.push(value); argQuoted = argQuoted || quoted; @@ -1425,18 +1475,18 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return commandLine; } - private getQuotingOptions(shellBasename: string, shellOptions: ShellConfiguration | undefined, platform: Platform.Platform): ShellQuotingOptions { + private _getQuotingOptions(shellBasename: string, shellOptions: IShellConfiguration | undefined, platform: Platform.Platform): IShellQuotingOptions { if (shellOptions && shellOptions.quoting) { return shellOptions.quoting; } - return TerminalTaskSystem.shellQuotes[shellBasename] || TerminalTaskSystem.osShellQuotes[Platform.PlatformToString(platform)]; + return TerminalTaskSystem._shellQuotes[shellBasename] || TerminalTaskSystem._osShellQuotes[Platform.PlatformToString(platform)]; } - private collectTaskVariables(variables: Set<string>, task: CustomTask | ContributedTask): void { + private _collectTaskVariables(variables: Set<string>, task: CustomTask | ContributedTask): void { if (task.command && task.command.name) { - this.collectCommandVariables(variables, task.command, task); + this._collectCommandVariables(variables, task.command, task); } - this.collectMatcherVariables(variables, task.configurationProperties.problemMatchers); + this._collectMatcherVariables(variables, task.configurationProperties.problemMatchers); if (task.command.runtime === RuntimeType.CustomExecution && (CustomTask.is(task) || ContributedTask.is(task))) { let definition: any; @@ -1447,23 +1497,23 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { delete definition._key; delete definition.type; } - this.collectDefinitionVariables(variables, definition); + this._collectDefinitionVariables(variables, definition); } } - private collectDefinitionVariables(variables: Set<string>, definition: any): void { + private _collectDefinitionVariables(variables: Set<string>, definition: any): void { if (Types.isString(definition)) { - this.collectVariables(variables, definition); + this._collectVariables(variables, definition); } else if (Types.isArray(definition)) { - definition.forEach((element: any) => this.collectDefinitionVariables(variables, element)); + definition.forEach((element: any) => this._collectDefinitionVariables(variables, element)); } else if (Types.isObject(definition)) { for (const key in definition) { - this.collectDefinitionVariables(variables, definition[key]); + this._collectDefinitionVariables(variables, definition[key]); } } } - private collectCommandVariables(variables: Set<string>, command: CommandConfiguration, task: CustomTask | ContributedTask): void { + private _collectCommandVariables(variables: Set<string>, command: ICommandConfiguration, task: CustomTask | ContributedTask): void { // The custom execution should have everything it needs already as it provided // the callback. if (command.runtime === RuntimeType.CustomExecution) { @@ -1473,41 +1523,37 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (command.name === undefined) { throw new Error('Command name should never be undefined here.'); } - this.collectVariables(variables, command.name); - if (command.args) { - command.args.forEach(arg => this.collectVariables(variables, arg)); - } + this._collectVariables(variables, command.name); + command.args?.forEach(arg => this._collectVariables(variables, arg)); // Try to get a scope. - const scope = (<ExtensionTaskSource>task._source).scope; + const scope = (<IExtensionTaskSource>task._source).scope; if (scope !== TaskScope.Global) { variables.add('${workspaceFolder}'); } if (command.options) { - let options = command.options; + const options = command.options; if (options.cwd) { - this.collectVariables(variables, options.cwd); + this._collectVariables(variables, options.cwd); } const optionsEnv = options.env; if (optionsEnv) { Object.keys(optionsEnv).forEach((key) => { - let value: any = optionsEnv[key]; + const value: any = optionsEnv[key]; if (Types.isString(value)) { - this.collectVariables(variables, value); + this._collectVariables(variables, value); } }); } if (options.shell) { if (options.shell.executable) { - this.collectVariables(variables, options.shell.executable); - } - if (options.shell.args) { - options.shell.args.forEach(arg => this.collectVariables(variables, arg)); + this._collectVariables(variables, options.shell.executable); } + options.shell.args?.forEach(arg => this._collectVariables(variables, arg)); } } } - private collectMatcherVariables(variables: Set<string>, values: Array<string | ProblemMatcher> | undefined): void { + private _collectMatcherVariables(variables: Set<string>, values: Array<string | ProblemMatcher> | undefined): void { if (values === undefined || values === null || values.length === 0) { return; } @@ -1523,14 +1569,14 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { matcher = value; } if (matcher && matcher.filePrefix) { - this.collectVariables(variables, matcher.filePrefix); + this._collectVariables(variables, matcher.filePrefix); } }); } - private collectVariables(variables: Set<string>, value: string | CommandString): void { - let string: string = Types.isString(value) ? value : value.value; - let r = /\$\{(.*?)\}/g; + private _collectVariables(variables: Set<string>, value: string | CommandString): void { + const string: string = Types.isString(value) ? value : value.value; + const r = /\$\{(.*?)\}/g; let matches: RegExpExecArray | null; do { matches = r.exec(string); @@ -1540,25 +1586,25 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } while (matches); } - private async resolveCommandAndArgs(resolver: VariableResolver, commandConfig: CommandConfiguration): Promise<{ command: CommandString; args: CommandString[] }> { + private async _resolveCommandAndArgs(resolver: VariableResolver, commandConfig: ICommandConfiguration): Promise<{ command: CommandString; args: CommandString[] }> { // First we need to use the command args: let args: CommandString[] = commandConfig.args ? commandConfig.args.slice() : []; - args = await this.resolveVariables(resolver, args); - let command: CommandString = await this.resolveVariable(resolver, commandConfig.name); + args = await this._resolveVariables(resolver, args); + const command: CommandString = await this._resolveVariable(resolver, commandConfig.name); return { command, args }; } - private async resolveVariables(resolver: VariableResolver, value: string[]): Promise<string[]>; - private async resolveVariables(resolver: VariableResolver, value: CommandString[]): Promise<CommandString[]>; - private async resolveVariables(resolver: VariableResolver, value: CommandString[]): Promise<CommandString[]> { - return Promise.all(value.map(s => this.resolveVariable(resolver, s))); + private async _resolveVariables(resolver: VariableResolver, value: string[]): Promise<string[]>; + private async _resolveVariables(resolver: VariableResolver, value: CommandString[]): Promise<CommandString[]>; + private async _resolveVariables(resolver: VariableResolver, value: CommandString[]): Promise<CommandString[]> { + return Promise.all(value.map(s => this._resolveVariable(resolver, s))); } - private async resolveMatchers(resolver: VariableResolver, values: Array<string | ProblemMatcher> | undefined): Promise<ProblemMatcher[]> { + private async _resolveMatchers(resolver: VariableResolver, values: Array<string | ProblemMatcher> | undefined): Promise<ProblemMatcher[]> { if (values === undefined || values === null || values.length === 0) { return []; } - let result: ProblemMatcher[] = []; + const result: ProblemMatcher[] = []; for (const value of values) { let matcher: ProblemMatcher; if (Types.isString(value)) { @@ -1571,21 +1617,21 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { matcher = value; } if (!matcher) { - this.appendOutput(nls.localize('unknownProblemMatcher', 'Problem matcher {0} can\'t be resolved. The matcher will be ignored')); + this._appendOutput(nls.localize('unknownProblemMatcher', 'Problem matcher {0} can\'t be resolved. The matcher will be ignored')); continue; } - let taskSystemInfo: TaskSystemInfo | undefined = resolver.taskSystemInfo; - let hasFilePrefix = matcher.filePrefix !== undefined; - let hasUriProvider = taskSystemInfo !== undefined && taskSystemInfo.uriProvider !== undefined; + const taskSystemInfo: ITaskSystemInfo | undefined = resolver.taskSystemInfo; + const hasFilePrefix = matcher.filePrefix !== undefined; + const hasUriProvider = taskSystemInfo !== undefined && taskSystemInfo.uriProvider !== undefined; if (!hasFilePrefix && !hasUriProvider) { result.push(matcher); } else { - let copy = Objects.deepClone(matcher); + const copy = Objects.deepClone(matcher); if (hasUriProvider && (taskSystemInfo !== undefined)) { copy.uriProvider = taskSystemInfo.uriProvider; } if (hasFilePrefix) { - copy.filePrefix = await this.resolveVariable(resolver, copy.filePrefix); + copy.filePrefix = await this._resolveVariable(resolver, copy.filePrefix); } result.push(copy); } @@ -1593,9 +1639,9 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return result; } - private async resolveVariable(resolver: VariableResolver, value: string | undefined): Promise<string>; - private async resolveVariable(resolver: VariableResolver, value: CommandString | undefined): Promise<CommandString>; - private async resolveVariable(resolver: VariableResolver, value: CommandString | undefined): Promise<CommandString> { + private async _resolveVariable(resolver: VariableResolver, value: string | undefined): Promise<string>; + private async _resolveVariable(resolver: VariableResolver, value: CommandString | undefined): Promise<CommandString>; + private async _resolveVariable(resolver: VariableResolver, value: CommandString | undefined): Promise<CommandString> { // TODO@Dirk Task.getWorkspaceFolder should return a WorkspaceFolder that is defined in workspace.ts if (Types.isString(value)) { return resolver.resolve(value); @@ -1609,25 +1655,25 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } } - private async resolveOptions(resolver: VariableResolver, options: CommandOptions | undefined): Promise<CommandOptions> { + private async _resolveOptions(resolver: VariableResolver, options: CommandOptions | undefined): Promise<CommandOptions> { if (options === undefined || options === null) { let cwd: string | undefined; try { - cwd = await this.resolveVariable(resolver, '${workspaceFolder}'); + cwd = await this._resolveVariable(resolver, '${workspaceFolder}'); } catch (e) { // No workspace } return { cwd }; } - let result: CommandOptions = Types.isString(options.cwd) - ? { cwd: await this.resolveVariable(resolver, options.cwd) } - : { cwd: await this.resolveVariable(resolver, '${workspaceFolder}') }; + const result: CommandOptions = Types.isString(options.cwd) + ? { cwd: await this._resolveVariable(resolver, options.cwd) } + : { cwd: await this._resolveVariable(resolver, '${workspaceFolder}') }; if (options.env) { result.env = Object.create(null); for (const key of Object.keys(options.env)) { - let value: any = options.env![key]; + const value: any = options.env![key]; if (Types.isString(value)) { - result.env![key] = await this.resolveVariable(resolver, value); + result.env![key] = await this._resolveVariable(resolver, value); } else { result.env![key] = value.toString(); } @@ -1636,7 +1682,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return result; } - private static WellKnowCommands: IStringDictionary<boolean> = { + static WellKnownCommands: IStringDictionary<boolean> = { 'ant': true, 'cmake': true, 'eslint': true, @@ -1659,20 +1705,24 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { public getSanitizedCommand(cmd: string): string { let result = cmd.toLowerCase(); - let index = result.lastIndexOf(path.sep); + const index = result.lastIndexOf(path.sep); if (index !== -1) { result = result.substring(index + 1); } - if (TerminalTaskSystem.WellKnowCommands[result]) { + if (TerminalTaskSystem.WellKnownCommands[result]) { return result; } return 'other'; } - private appendOutput(output: string): void { - const outputChannel = this.outputService.getChannel(this.outputChannelId); - if (outputChannel) { - outputChannel.append(output); - } + private _appendOutput(output: string): void { + const outputChannel = this._outputService.getChannel(this._outputChannelId); + outputChannel?.append(output); } } + +function taskShellIntegrationWaitOnExitSequence(message: string): (exitCode: number) => string { + return (exitCode) => { + return `${VSCodeSequence(VSCodeOscPt.CommandFinished, exitCode.toString())}${message}`; + }; +} diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts index caeb77b3a62..de1ff5244cf 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v1.ts @@ -63,13 +63,13 @@ const shellCommand: IJSONSchema = { }; schema.definitions = Objects.deepClone(commonSchema.definitions); -let definitions = schema.definitions!; +const definitions = schema.definitions!; definitions['commandConfiguration']['properties']!['isShellCommand'] = Objects.deepClone(shellCommand); definitions['taskDescription']['properties']!['isShellCommand'] = Objects.deepClone(shellCommand); definitions['taskRunnerConfiguration']['properties']!['isShellCommand'] = Objects.deepClone(shellCommand); Object.getOwnPropertyNames(definitions).forEach(key => { - let newKey = key + '1'; + const newKey = key + '1'; definitions[newKey] = definitions[key]; delete definitions[key]; }); @@ -82,7 +82,7 @@ function fixReferences(literal: any) { literal['$ref'] = literal['$ref'] + '1'; } Object.getOwnPropertyNames(literal).forEach(property => { - let value = literal[property]; + const value = literal[property]; if (Array.isArray(value) || typeof value === 'object') { fixReferences(value); } @@ -93,7 +93,7 @@ fixReferences(schema); ProblemMatcherRegistry.onReady().then(() => { try { - let matcherIds = ProblemMatcherRegistry.keys().map(key => '$' + key); + const matcherIds = ProblemMatcherRegistry.keys().map(key => '$' + key); definitions.problemMatcherType1.oneOf![0].enum = matcherIds; (definitions.problemMatcherType1.oneOf![2].items as IJSONSchema).anyOf![1].enum = matcherIds; } catch (err) { diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index f0ba5e9fa63..077bd89a60e 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -13,6 +13,7 @@ import { ProblemMatcherRegistry } from 'vs/workbench/contrib/tasks/common/proble import { TaskDefinitionRegistry } from './taskDefinitionRegistry'; import * as ConfigurationResolverUtils from 'vs/workbench/services/configurationResolver/common/configurationResolverUtils'; import { inputsSchema } from 'vs/workbench/services/configurationResolver/common/configurationResolverSchema'; +import { Codicon } from 'vs/base/common/codicons'; function fixReferences(literal: any) { if (Array.isArray(literal)) { @@ -22,7 +23,7 @@ function fixReferences(literal: any) { literal['$ref'] = literal['$ref'] + '2'; } Object.getOwnPropertyNames(literal).forEach(property => { - let value = literal[property]; + const value = literal[property]; if (Array.isArray(value) || typeof value === 'object') { fixReferences(value); } @@ -94,6 +95,33 @@ const detail: IJSONSchema = { description: nls.localize('JsonSchema.tasks.detail', 'An optional description of a task that shows in the Run Task quick pick as a detail.') }; +const icon: IJSONSchema = { + type: 'object', + description: nls.localize('JsonSchema.tasks.icon', 'An optional icon for the task'), + properties: { + id: { + description: nls.localize('JsonSchema.tasks.icon.id', 'An optional codicon ID to use'), + type: ['string', 'null'], + enum: Array.from(Codicon.getAll(), icon => icon.id), + markdownEnumDescriptions: Array.from(Codicon.getAll(), icon => `$(${icon.id})`), + }, + color: { + description: nls.localize('JsonSchema.tasks.icon.color', 'An optional color of the icon'), + type: ['string', 'null'], + enum: [ + 'terminal.ansiBlack', + 'terminal.ansiRed', + 'terminal.ansiGreen', + 'terminal.ansiYellow', + 'terminal.ansiBlue', + 'terminal.ansiMagenta', + 'terminal.ansiCyan', + 'terminal.ansiWhite' + ], + }, + } +}; + const presentation: IJSONSchema = { type: 'object', default: { @@ -352,7 +380,7 @@ const options: IJSONSchema = Objects.deepClone(commonSchemaDefinitions.options); const optionsProperties = options.properties!; optionsProperties.shell = Objects.deepClone(commonSchemaDefinitions.shellConfiguration); -let taskConfiguration: IJSONSchema = { +const taskConfiguration: IJSONSchema = { type: 'object', additionalProperties: false, properties: { @@ -378,6 +406,7 @@ let taskConfiguration: IJSONSchema = { default: false }, presentation: Objects.deepClone(presentation), + icon: Objects.deepClone(icon), options: options, problemMatcher: { $ref: '#/definitions/problemMatcherType', @@ -390,13 +419,13 @@ let taskConfiguration: IJSONSchema = { } }; -let taskDefinitions: IJSONSchema[] = []; +const taskDefinitions: IJSONSchema[] = []; TaskDefinitionRegistry.onReady().then(() => { updateTaskDefinitions(); }); export function updateTaskDefinitions() { - for (let taskType of TaskDefinitionRegistry.all()) { + for (const taskType of TaskDefinitionRegistry.all()) { // Check that we haven't already added this task type if (taskDefinitions.find(schema => { return schema.properties?.type?.enum?.find ? schema.properties?.type.enum.find(element => element === taskType.taskType) : undefined; @@ -404,7 +433,7 @@ export function updateTaskDefinitions() { continue; } - let schema: IJSONSchema = Objects.deepClone(taskConfiguration); + const schema: IJSONSchema = Objects.deepClone(taskConfiguration); const schemaProperties = schema.properties!; // Since we do this after the schema is assigned we need to patch the refs. schemaProperties.type = { @@ -420,8 +449,8 @@ export function updateTaskDefinitions() { // Customized tasks require that the task type be set. schema.required.push('type'); if (taskType.properties) { - for (let key of Object.keys(taskType.properties)) { - let property = taskType.properties[key]; + for (const key of Object.keys(taskType.properties)) { + const property = taskType.properties[key]; schemaProperties[key] = Objects.deepClone(property); } } @@ -430,7 +459,7 @@ export function updateTaskDefinitions() { } } -let customize = Objects.deepClone(taskConfiguration); +const customize = Objects.deepClone(taskConfiguration); customize.properties!.customize = { type: 'string', deprecationMessage: nls.localize('JsonSchema.tasks.customize.deprecated', 'The customize property is deprecated. See the 1.14 release notes on how to migrate to the new task customization approach') @@ -441,8 +470,8 @@ if (!customize.required) { customize.required.push('customize'); taskDefinitions.push(customize); -let definitions = Objects.deepClone(commonSchemaDefinitions); -let taskDescription: IJSONSchema = definitions.taskDescription; +const definitions = Objects.deepClone(commonSchemaDefinitions); +const taskDescription: IJSONSchema = definitions.taskDescription; taskDescription.required = ['label']; const taskDescriptionProperties = taskDescription.properties!; taskDescriptionProperties.label = Objects.deepClone(label); @@ -455,6 +484,7 @@ taskDescriptionProperties.identifier = Objects.deepClone(identifier); taskDescriptionProperties.type = Objects.deepClone(taskType); taskDescriptionProperties.presentation = Objects.deepClone(presentation); taskDescriptionProperties.terminal = terminal; +taskDescriptionProperties.icon = Objects.deepClone(icon); taskDescriptionProperties.group = Objects.deepClone(group); taskDescriptionProperties.runOptions = Objects.deepClone(runOptions); taskDescriptionProperties.detail = detail; @@ -508,7 +538,7 @@ taskDefinitions.push({ } as IJSONSchema); const definitionsTaskRunnerConfigurationProperties = definitions.taskRunnerConfiguration.properties!; -let tasks = definitionsTaskRunnerConfigurationProperties.tasks; +const tasks = definitionsTaskRunnerConfigurationProperties.tasks; tasks.items = { oneOf: taskDefinitions }; @@ -534,7 +564,7 @@ definitionsTaskRunnerConfigurationProperties.taskSelector.deprecationMessage = n 'The property taskSelector is deprecated. Inline the command with its arguments into the task instead. See also the 1.14 release notes.' ); -let osSpecificTaskRunnerConfiguration = Objects.deepClone(definitions.taskRunnerConfiguration); +const osSpecificTaskRunnerConfiguration = Objects.deepClone(definitions.taskRunnerConfiguration); delete osSpecificTaskRunnerConfiguration.properties!.tasks; osSpecificTaskRunnerConfiguration.additionalProperties = false; definitions.osSpecificTaskRunnerConfiguration = osSpecificTaskRunnerConfiguration; @@ -585,7 +615,7 @@ function deprecatedVariableMessage(schemaMap: IJSONSchemaMap, property: string) } Object.getOwnPropertyNames(definitions).forEach(key => { - let newKey = key + '2'; + const newKey = key + '2'; definitions[newKey] = definitions[key]; delete definitions[key]; deprecatedVariableMessage(definitions, newKey); @@ -594,7 +624,7 @@ fixReferences(schema); export function updateProblemMatchers() { try { - let matcherIds = ProblemMatcherRegistry.keys().map(key => '$' + key); + const matcherIds = ProblemMatcherRegistry.keys().map(key => '$' + key); definitions.problemMatcherType2.oneOf![0].enum = matcherIds; (definitions.problemMatcherType2.oneOf![2].items as IJSONSchema).anyOf![0].enum = matcherIds; } catch (err) { diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index 081843db236..7bf4c6ae71f 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -10,7 +10,7 @@ import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/model'; -import { ILineMatcher, createLineMatcher, ProblemMatcher, ProblemMatch, ApplyToKind, WatchingPattern, getResource } from 'vs/workbench/contrib/tasks/common/problemMatcher'; +import { ILineMatcher, createLineMatcher, ProblemMatcher, IProblemMatch, ApplyToKind, IWatchingPattern, getResource } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { IMarkerService, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { generateUuid } from 'vs/base/common/uuid'; import { IFileService } from 'vs/platform/files/common/files'; @@ -21,11 +21,11 @@ export const enum ProblemCollectorEventKind { BackgroundProcessingEnds = 'backgroundProcessingEnds' } -export interface ProblemCollectorEvent { +export interface IProblemCollectorEvent { kind: ProblemCollectorEventKind; } -namespace ProblemCollectorEvent { +namespace IProblemCollectorEvent { export function create(kind: ProblemCollectorEventKind) { return Object.freeze({ kind }); } @@ -39,7 +39,7 @@ export abstract class AbstractProblemCollector implements IDisposable { private matchers: INumberDictionary<ILineMatcher[]>; private activeMatcher: ILineMatcher | null; - private _numberOfMatches: number; + protected _numberOfMatches: number; private _maxMarkerSeverity?: MarkerSeverity; private buffer: string[]; private bufferLength: number; @@ -56,13 +56,22 @@ export abstract class AbstractProblemCollector implements IDisposable { // [owner] -> [resource] -> number; private deliveredMarkers: Map<string, Map<string, number>>; - protected _onDidStateChange: Emitter<ProblemCollectorEvent>; + protected _onDidStateChange: Emitter<IProblemCollectorEvent>; + + protected readonly _onDidFindFirstMatch = new Emitter<void>(); + readonly onDidFindFirstMatch = this._onDidFindFirstMatch.event; + + protected readonly _onDidFindErrors = new Emitter<void>(); + readonly onDidFindErrors = this._onDidFindErrors.event; + + protected readonly _onDidRequestInvalidateLastMarker = new Emitter<void>(); + readonly onDidRequestInvalidateLastMarker = this._onDidRequestInvalidateLastMarker.event; constructor(public readonly problemMatchers: ProblemMatcher[], protected markerService: IMarkerService, protected modelService: IModelService, fileService?: IFileService) { this.matchers = Object.create(null); this.bufferLength = 1; problemMatchers.map(elem => createLineMatcher(elem, fileService)).forEach((matcher) => { - let length = matcher.matchLength; + const length = matcher.matchLength; if (length > this.bufferLength) { this.bufferLength = length; } @@ -79,8 +88,8 @@ export abstract class AbstractProblemCollector implements IDisposable { this._maxMarkerSeverity = undefined; this.openModels = Object.create(null); this.applyToByOwner = new Map<string, ApplyToKind>(); - for (let problemMatcher of problemMatchers) { - let current = this.applyToByOwner.get(problemMatcher.owner); + for (const problemMatcher of problemMatchers) { + const current = this.applyToByOwner.get(problemMatcher.owner); if (current === undefined) { this.applyToByOwner.set(problemMatcher.owner, problemMatcher.applyTo); } else { @@ -101,7 +110,7 @@ export abstract class AbstractProblemCollector implements IDisposable { this._onDidStateChange = new Emitter(); } - public get onDidStateChange(): Event<ProblemCollectorEvent> { + public get onDidStateChange(): Event<IProblemCollectorEvent> { return this._onDidStateChange.event; } @@ -130,8 +139,8 @@ export abstract class AbstractProblemCollector implements IDisposable { return this._maxMarkerSeverity; } - protected tryFindMarker(line: string): ProblemMatch | null { - let result: ProblemMatch | null = null; + protected tryFindMarker(line: string): IProblemMatch | null { + let result: IProblemMatch | null = null; if (this.activeMatcher) { result = this.activeMatcher.next(line); if (result) { @@ -144,7 +153,7 @@ export abstract class AbstractProblemCollector implements IDisposable { if (this.buffer.length < this.bufferLength) { this.buffer.push(line); } else { - let end = this.buffer.length - 1; + const end = this.buffer.length - 1; for (let i = 0; i < end; i++) { this.buffer[i] = this.buffer[i + 1]; } @@ -158,7 +167,7 @@ export abstract class AbstractProblemCollector implements IDisposable { return result; } - protected async shouldApplyMatch(result: ProblemMatch): Promise<boolean> { + protected async shouldApplyMatch(result: IProblemMatch): Promise<boolean> { switch (result.description.applyTo) { case ApplyToKind.allDocuments: return true; @@ -178,16 +187,16 @@ export abstract class AbstractProblemCollector implements IDisposable { return ApplyToKind.allDocuments; } - private tryMatchers(): ProblemMatch | null { + private tryMatchers(): IProblemMatch | null { this.activeMatcher = null; - let length = this.buffer.length; + const length = this.buffer.length; for (let startIndex = 0; startIndex < length; startIndex++) { - let candidates = this.matchers[length - startIndex]; + const candidates = this.matchers[length - startIndex]; if (!candidates) { continue; } for (const matcher of candidates) { - let result = matcher.handle(this.buffer, startIndex); + const result = matcher.handle(this.buffer, startIndex); if (result.match) { this.captureMatch(result.match); if (result.continue) { @@ -200,7 +209,7 @@ export abstract class AbstractProblemCollector implements IDisposable { return null; } - private captureMatch(match: ProblemMatch): void { + private captureMatch(match: IProblemMatch): void { this._numberOfMatches++; if (this._maxMarkerSeverity === undefined || match.marker.severity > this._maxMarkerSeverity) { this._maxMarkerSeverity = match.marker.severity; @@ -214,7 +223,7 @@ export abstract class AbstractProblemCollector implements IDisposable { } protected recordResourcesToClean(owner: string): void { - let resourceSetToClean = this.getResourceSetToClean(owner); + const resourceSetToClean = this.getResourceSetToClean(owner); this.markerService.read({ owner: owner }).forEach(marker => resourceSetToClean.set(marker.resource.toString(), marker.resource)); } @@ -223,10 +232,8 @@ export abstract class AbstractProblemCollector implements IDisposable { } protected removeResourceToClean(owner: string, resource: string): void { - let resourceSet = this.resourcesToClean.get(owner); - if (resourceSet) { - resourceSet.delete(resource); - } + const resourceSet = this.resourcesToClean.get(owner); + resourceSet?.delete(resource); } private getResourceSetToClean(owner: string): Map<string, URI> { @@ -246,7 +253,7 @@ export abstract class AbstractProblemCollector implements IDisposable { } protected cleanMarkers(owner: string): void { - let toClean = this.resourcesToClean.get(owner); + const toClean = this.resourcesToClean.get(owner); if (toClean) { this._cleanMarkers(owner, toClean); this.resourcesToClean.delete(owner); @@ -254,8 +261,8 @@ export abstract class AbstractProblemCollector implements IDisposable { } private _cleanMarkers(owner: string, toClean: Map<string, URI>): void { - let uris: URI[] = []; - let applyTo = this.applyToByOwner.get(owner); + const uris: URI[] = []; + const applyTo = this.applyToByOwner.get(owner); toClean.forEach((uri, uriAsString) => { if ( applyTo === ApplyToKind.allDocuments || @@ -279,7 +286,7 @@ export abstract class AbstractProblemCollector implements IDisposable { markersPerResource = new Map<string, IMarkerData>(); markersPerOwner.set(resourceAsString, markersPerResource); } - let key = IMarkerData.makeKeyOptionalMessage(marker, false); + const key = IMarkerData.makeKeyOptionalMessage(marker, false); let existingMarker; if (!markersPerResource.has(key)) { markersPerResource.set(key, marker); @@ -292,7 +299,7 @@ export abstract class AbstractProblemCollector implements IDisposable { protected reportMarkers(): void { this.markers.forEach((markersPerOwner, owner) => { - let deliveredMarkersPerOwner = this.getDeliveredMarkersPerOwner(owner); + const deliveredMarkersPerOwner = this.getDeliveredMarkersPerOwner(owner); markersPerOwner.forEach((markers, resource) => { this.deliverMarkersPerOwnerAndResourceResolved(owner, resource, markers, deliveredMarkersPerOwner); }); @@ -300,12 +307,12 @@ export abstract class AbstractProblemCollector implements IDisposable { } protected deliverMarkersPerOwnerAndResource(owner: string, resource: string): void { - let markersPerOwner = this.markers.get(owner); + const markersPerOwner = this.markers.get(owner); if (!markersPerOwner) { return; } - let deliveredMarkersPerOwner = this.getDeliveredMarkersPerOwner(owner); - let markersPerResource = markersPerOwner.get(resource); + const deliveredMarkersPerOwner = this.getDeliveredMarkersPerOwner(owner); + const markersPerResource = markersPerOwner.get(resource); if (!markersPerResource) { return; } @@ -314,7 +321,7 @@ export abstract class AbstractProblemCollector implements IDisposable { private deliverMarkersPerOwnerAndResourceResolved(owner: string, resource: string, markers: Map<string, IMarkerData>, reported: Map<string, number>): void { if (markers.size !== reported.get(resource)) { - let toSet: IMarkerData[] = []; + const toSet: IMarkerData[] = []; markers.forEach(value => toSet.push(value)); this.markerService.changeOne(owner, URI.parse(resource), toSet); reported.set(resource, markers.size); @@ -355,7 +362,7 @@ export class StartStopProblemCollector extends AbstractProblemCollector implemen constructor(problemMatchers: ProblemMatcher[], markerService: IMarkerService, modelService: IModelService, _strategy: ProblemHandlingStrategy = ProblemHandlingStrategy.Clean, fileService?: IFileService) { super(problemMatchers, markerService, modelService, fileService); - let ownerSet: { [key: string]: boolean } = Object.create(null); + const ownerSet: { [key: string]: boolean } = Object.create(null); problemMatchers.forEach(description => ownerSet[description.owner] = true); this.owners = Object.keys(ownerSet); this.owners.forEach((owner) => { @@ -364,16 +371,16 @@ export class StartStopProblemCollector extends AbstractProblemCollector implemen } protected async processLineInternal(line: string): Promise<void> { - let markerMatch = this.tryFindMarker(line); + const markerMatch = this.tryFindMarker(line); if (!markerMatch) { return; } - let owner = markerMatch.description.owner; - let resource = await markerMatch.resource; - let resourceAsString = resource.toString(); + const owner = markerMatch.description.owner; + const resource = await markerMatch.resource; + const resourceAsString = resource.toString(); this.removeResourceToClean(owner, resourceAsString); - let shouldApplyMatch = await this.shouldApplyMatch(markerMatch); + const shouldApplyMatch = await this.shouldApplyMatch(markerMatch); if (shouldApplyMatch) { this.recordMarker(markerMatch.marker, owner, resourceAsString); if (this.currentOwner !== owner || this.currentResource !== resourceAsString) { @@ -387,16 +394,16 @@ export class StartStopProblemCollector extends AbstractProblemCollector implemen } } -interface BackgroundPatterns { +interface IBackgroundPatterns { key: string; matcher: ProblemMatcher; - begin: WatchingPattern; - end: WatchingPattern; + begin: IWatchingPattern; + end: IWatchingPattern; } export class WatchingProblemCollector extends AbstractProblemCollector implements IProblemMatcher { - private backgroundPatterns: BackgroundPatterns[]; + private backgroundPatterns: IBackgroundPatterns[]; // workaround for https://github.com/microsoft/vscode/issues/44018 private _activeBackgroundMatchers: Set<string>; @@ -447,10 +454,10 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement } public aboutToStart(): void { - for (let background of this.backgroundPatterns) { + for (const background of this.backgroundPatterns) { if (background.matcher.watching && background.matcher.watching.activeOnStart) { this._activeBackgroundMatchers.add(background.key); - this._onDidStateChange.fire(ProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingBegins)); + this._onDidStateChange.fire(IProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingBegins)); this.recordResourcesToClean(background.matcher.owner); } } @@ -461,15 +468,15 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement return; } this.lines.push(line); - let markerMatch = this.tryFindMarker(line); + const markerMatch = this.tryFindMarker(line); if (!markerMatch) { return; } - let resource = await markerMatch.resource; - let owner = markerMatch.description.owner; - let resourceAsString = resource.toString(); + const resource = await markerMatch.resource; + const owner = markerMatch.description.owner; + const resourceAsString = resource.toString(); this.removeResourceToClean(owner, resourceAsString); - let shouldApplyMatch = await this.shouldApplyMatch(markerMatch); + const shouldApplyMatch = await this.shouldApplyMatch(markerMatch); if (shouldApplyMatch) { this.recordMarker(markerMatch.marker, owner, resourceAsString); if (this.currentOwner !== owner || this.currentResource !== resourceAsString) { @@ -487,22 +494,23 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement private async tryBegin(line: string): Promise<boolean> { let result = false; for (const background of this.backgroundPatterns) { - let matches = background.begin.regexp.exec(line); + const matches = background.begin.regexp.exec(line); if (matches) { if (this._activeBackgroundMatchers.has(background.key)) { continue; } this._activeBackgroundMatchers.add(background.key); result = true; + this._onDidFindFirstMatch.fire(); this.lines = []; this.lines.push(line); - this._onDidStateChange.fire(ProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingBegins)); + this._onDidStateChange.fire(IProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingBegins)); this.cleanMarkerCaches(); this.resetCurrentResource(); - let owner = background.matcher.owner; - let file = matches[background.begin.file!]; + const owner = background.matcher.owner; + const file = matches[background.begin.file!]; if (file) { - let resource = getResource(file, background.matcher); + const resource = getResource(file, background.matcher); this.recordResourceToClean(owner, await resource); } else { this.recordResourcesToClean(owner); @@ -515,15 +523,20 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement private tryFinish(line: string): boolean { let result = false; for (const background of this.backgroundPatterns) { - let matches = background.end.regexp.exec(line); + const matches = background.end.regexp.exec(line); if (matches) { + if (this._numberOfMatches > 0) { + this._onDidFindErrors.fire(); + } else { + this._onDidRequestInvalidateLastMarker.fire(); + } if (this._activeBackgroundMatchers.has(background.key)) { this._activeBackgroundMatchers.delete(background.key); this.resetCurrentResource(); - this._onDidStateChange.fire(ProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingEnds)); + this._onDidStateChange.fire(IProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingEnds)); result = true; this.lines.push(line); - let owner = background.matcher.owner; + const owner = background.matcher.owner; this.cleanMarkers(owner); this.cleanMarkerCaches(); } diff --git a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts index 296d268d888..7e2fb354129 100644 --- a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts @@ -63,7 +63,7 @@ export module ProblemLocationKind { } } -export interface ProblemPattern { +export interface IProblemPattern { regexp: RegExp; kind?: ProblemLocationKind; @@ -89,21 +89,21 @@ export interface ProblemPattern { loop?: boolean; } -export interface NamedProblemPattern extends ProblemPattern { +export interface INamedProblemPattern extends IProblemPattern { name: string; } -export type MultiLineProblemPattern = ProblemPattern[]; +export type MultiLineProblemPattern = IProblemPattern[]; -export interface WatchingPattern { +export interface IWatchingPattern { regexp: RegExp; file?: number; } -export interface WatchingMatcher { +export interface IWatchingMatcher { activeOnStart: boolean; - beginsPattern: WatchingPattern; - endsPattern: WatchingPattern; + beginsPattern: IWatchingPattern; + endsPattern: IWatchingPattern; } export enum ApplyToKind { @@ -133,36 +133,36 @@ export interface ProblemMatcher { applyTo: ApplyToKind; fileLocation: FileLocationKind; filePrefix?: string; - pattern: ProblemPattern | ProblemPattern[]; + pattern: IProblemPattern | IProblemPattern[]; severity?: Severity; - watching?: WatchingMatcher; + watching?: IWatchingMatcher; uriProvider?: (path: string) => URI; } -export interface NamedProblemMatcher extends ProblemMatcher { +export interface INamedProblemMatcher extends ProblemMatcher { name: string; label: string; deprecated?: boolean; } -export interface NamedMultiLineProblemPattern { +export interface INamedMultiLineProblemPattern { name: string; label: string; patterns: MultiLineProblemPattern; } -export function isNamedProblemMatcher(value: ProblemMatcher | undefined): value is NamedProblemMatcher { - return value && Types.isString((<NamedProblemMatcher>value).name) ? true : false; +export function isNamedProblemMatcher(value: ProblemMatcher | undefined): value is INamedProblemMatcher { + return value && Types.isString((<INamedProblemMatcher>value).name) ? true : false; } -interface Location { +interface ILocation { startLineNumber: number; startCharacter: number; endLineNumber: number; endCharacter: number; } -interface ProblemData { +interface IProblemData { kind?: ProblemLocationKind; file?: string; location?: string; @@ -175,20 +175,20 @@ interface ProblemData { code?: string; } -export interface ProblemMatch { +export interface IProblemMatch { resource: Promise<URI>; marker: IMarkerData; description: ProblemMatcher; } -export interface HandleResult { - match: ProblemMatch | null; +export interface IHandleResult { + match: IProblemMatch | null; continue: boolean; } export async function getResource(filename: string, matcher: ProblemMatcher, fileService?: IFileService): Promise<URI> { - let kind = matcher.fileLocation; + const kind = matcher.fileLocation; let fullPath: string | undefined; if (kind === FileLocationKind.Absolute) { fullPath = filename; @@ -230,12 +230,12 @@ export async function getResource(filename: string, matcher: ProblemMatcher, fil export interface ILineMatcher { matchLength: number; - next(line: string): ProblemMatch | null; - handle(lines: string[], start?: number): HandleResult; + next(line: string): IProblemMatch | null; + handle(lines: string[], start?: number): IHandleResult; } export function createLineMatcher(matcher: ProblemMatcher, fileService?: IFileService): ILineMatcher { - let pattern = matcher.pattern; + const pattern = matcher.pattern; if (Types.isArray(pattern)) { return new MultiLineMatcher(matcher, fileService); } else { @@ -254,17 +254,17 @@ abstract class AbstractLineMatcher implements ILineMatcher { this.fileService = fileService; } - public handle(lines: string[], start: number = 0): HandleResult { + public handle(lines: string[], start: number = 0): IHandleResult { return { match: null, continue: false }; } - public next(line: string): ProblemMatch | null { + public next(line: string): IProblemMatch | null { return null; } public abstract get matchLength(): number; - protected fillProblemData(data: ProblemData | undefined, pattern: ProblemPattern, matches: RegExpExecArray): data is ProblemData { + protected fillProblemData(data: IProblemData | undefined, pattern: IProblemPattern, matches: RegExpExecArray): data is IProblemData { if (data) { this.fillProperty(data, 'file', pattern, matches, true); this.appendProperty(data, 'message', pattern, matches, true); @@ -281,7 +281,7 @@ abstract class AbstractLineMatcher implements ILineMatcher { } } - private appendProperty(data: ProblemData, property: keyof ProblemData, pattern: ProblemPattern, matches: RegExpExecArray, trim: boolean = false): void { + private appendProperty(data: IProblemData, property: keyof IProblemData, pattern: IProblemPattern, matches: RegExpExecArray, trim: boolean = false): void { const patternProperty = pattern[property]; if (Types.isUndefined(data[property])) { this.fillProperty(data, property, pattern, matches, trim); @@ -295,7 +295,7 @@ abstract class AbstractLineMatcher implements ILineMatcher { } } - private fillProperty(data: ProblemData, property: keyof ProblemData, pattern: ProblemPattern, matches: RegExpExecArray, trim: boolean = false): void { + private fillProperty(data: IProblemData, property: keyof IProblemData, pattern: IProblemPattern, matches: RegExpExecArray, trim: boolean = false): void { const patternAtProperty = pattern[property]; if (Types.isUndefined(data[property]) && !Types.isUndefined(patternAtProperty) && patternAtProperty < matches.length) { let value = matches[patternAtProperty]; @@ -308,11 +308,11 @@ abstract class AbstractLineMatcher implements ILineMatcher { } } - protected getMarkerMatch(data: ProblemData): ProblemMatch | undefined { + protected getMarkerMatch(data: IProblemData): IProblemMatch | undefined { try { - let location = this.getLocation(data); + const location = this.getLocation(data); if (data.file && location && data.message) { - let marker: IMarkerData = { + const marker: IMarkerData = { severity: this.getSeverity(data), startLineNumber: location.startLineNumber, startColumn: location.startCharacter, @@ -342,7 +342,7 @@ abstract class AbstractLineMatcher implements ILineMatcher { return getResource(filename, this.matcher, this.fileService); } - private getLocation(data: ProblemData): Location | null { + private getLocation(data: IProblemData): ILocation | null { if (data.kind === ProblemLocationKind.File) { return this.createLocation(0, 0, 0, 0); } @@ -352,20 +352,20 @@ abstract class AbstractLineMatcher implements ILineMatcher { if (!data.line) { return null; } - let startLine = parseInt(data.line); - let startColumn = data.character ? parseInt(data.character) : undefined; - let endLine = data.endLine ? parseInt(data.endLine) : undefined; - let endColumn = data.endCharacter ? parseInt(data.endCharacter) : undefined; + const startLine = parseInt(data.line); + const startColumn = data.character ? parseInt(data.character) : undefined; + const endLine = data.endLine ? parseInt(data.endLine) : undefined; + const endColumn = data.endCharacter ? parseInt(data.endCharacter) : undefined; return this.createLocation(startLine, startColumn, endLine, endColumn); } - private parseLocationInfo(value: string): Location | null { + private parseLocationInfo(value: string): ILocation | null { if (!value || !value.match(/(\d+|\d+,\d+|\d+,\d+,\d+,\d+)/)) { return null; } - let parts = value.split(','); - let startLine = parseInt(parts[0]); - let startColumn = parts.length > 1 ? parseInt(parts[1]) : undefined; + const parts = value.split(','); + const startLine = parseInt(parts[0]); + const startColumn = parts.length > 1 ? parseInt(parts[1]) : undefined; if (parts.length > 3) { return this.createLocation(startLine, startColumn, parseInt(parts[2]), parseInt(parts[3])); } else { @@ -373,7 +373,7 @@ abstract class AbstractLineMatcher implements ILineMatcher { } } - private createLocation(startLine: number, startColumn: number | undefined, endLine: number | undefined, endColumn: number | undefined): Location { + private createLocation(startLine: number, startColumn: number | undefined, endLine: number | undefined, endColumn: number | undefined): ILocation { if (startColumn !== undefined && endColumn !== undefined) { return { startLineNumber: startLine, startCharacter: startColumn, endLineNumber: endLine || startLine, endCharacter: endColumn }; } @@ -383,10 +383,10 @@ abstract class AbstractLineMatcher implements ILineMatcher { return { startLineNumber: startLine, startCharacter: 1, endLineNumber: startLine, endCharacter: 2 ** 31 - 1 }; // See https://github.com/microsoft/vscode/issues/80288#issuecomment-650636442 for discussion } - private getSeverity(data: ProblemData): MarkerSeverity { + private getSeverity(data: IProblemData): MarkerSeverity { let result: Severity | null = null; if (data.severity) { - let value = data.severity; + const value = data.severity; if (value) { result = Severity.fromValue(value); if (result === Severity.Ignore) { @@ -413,27 +413,27 @@ abstract class AbstractLineMatcher implements ILineMatcher { class SingleLineMatcher extends AbstractLineMatcher { - private pattern: ProblemPattern; + private pattern: IProblemPattern; constructor(matcher: ProblemMatcher, fileService?: IFileService) { super(matcher, fileService); - this.pattern = <ProblemPattern>matcher.pattern; + this.pattern = <IProblemPattern>matcher.pattern; } public get matchLength(): number { return 1; } - public override handle(lines: string[], start: number = 0): HandleResult { + public override handle(lines: string[], start: number = 0): IHandleResult { Assert.ok(lines.length - start === 1); - let data: ProblemData = Object.create(null); + const data: IProblemData = Object.create(null); if (this.pattern.kind !== undefined) { data.kind = this.pattern.kind; } - let matches = this.pattern.regexp.exec(lines[start]); + const matches = this.pattern.regexp.exec(lines[start]); if (matches) { this.fillProblemData(data, this.pattern, matches); - let match = this.getMarkerMatch(data); + const match = this.getMarkerMatch(data); if (match) { return { match: match, continue: false }; } @@ -441,33 +441,33 @@ class SingleLineMatcher extends AbstractLineMatcher { return { match: null, continue: false }; } - public override next(line: string): ProblemMatch | null { + public override next(line: string): IProblemMatch | null { return null; } } class MultiLineMatcher extends AbstractLineMatcher { - private patterns: ProblemPattern[]; - private data: ProblemData | undefined; + private patterns: IProblemPattern[]; + private data: IProblemData | undefined; constructor(matcher: ProblemMatcher, fileService?: IFileService) { super(matcher, fileService); - this.patterns = <ProblemPattern[]>matcher.pattern; + this.patterns = <IProblemPattern[]>matcher.pattern; } public get matchLength(): number { return this.patterns.length; } - public override handle(lines: string[], start: number = 0): HandleResult { + public override handle(lines: string[], start: number = 0): IHandleResult { Assert.ok(lines.length - start === this.patterns.length); this.data = Object.create(null); let data = this.data!; data.kind = this.patterns[0].kind; for (let i = 0; i < this.patterns.length; i++) { - let pattern = this.patterns[i]; - let matches = pattern.regexp.exec(lines[i + start]); + const pattern = this.patterns[i]; + const matches = pattern.regexp.exec(lines[i + start]); if (!matches) { return { match: null, continue: false }; } else { @@ -478,7 +478,7 @@ class MultiLineMatcher extends AbstractLineMatcher { this.fillProblemData(data, pattern, matches); } } - let loop = !!this.patterns[this.patterns.length - 1].loop; + const loop = !!this.patterns[this.patterns.length - 1].loop; if (!loop) { this.data = undefined; } @@ -486,16 +486,16 @@ class MultiLineMatcher extends AbstractLineMatcher { return { match: markerMatch ? markerMatch : null, continue: loop }; } - public override next(line: string): ProblemMatch | null { - let pattern = this.patterns[this.patterns.length - 1]; + public override next(line: string): IProblemMatch | null { + const pattern = this.patterns[this.patterns.length - 1]; Assert.ok(pattern.loop === true && this.data !== null); - let matches = pattern.regexp.exec(line); + const matches = pattern.regexp.exec(line); if (!matches) { this.data = undefined; return null; } - let data = Objects.deepClone(this.data); - let problemMatch: ProblemMatch | undefined; + const data = Objects.deepClone(this.data); + let problemMatch: IProblemMatch | undefined; if (this.fillProblemData(data, pattern, matches)) { problemMatch = this.getMarkerMatch(data); } @@ -505,7 +505,7 @@ class MultiLineMatcher extends AbstractLineMatcher { export namespace Config { - export interface ProblemPattern { + export interface IProblemPattern { /** * The regular expression to find a problem in the console output of an @@ -591,7 +591,7 @@ export namespace Config { loop?: boolean; } - export interface CheckedProblemPattern extends ProblemPattern { + export interface ICheckedProblemPattern extends IProblemPattern { /** * The regular expression to find a problem in the console output of an * executed task. @@ -600,13 +600,13 @@ export namespace Config { } export namespace CheckedProblemPattern { - export function is(value: any): value is CheckedProblemPattern { - let candidate: ProblemPattern = value as ProblemPattern; + export function is(value: any): value is ICheckedProblemPattern { + const candidate: IProblemPattern = value as IProblemPattern; return candidate && Types.isString(candidate.regexp); } } - export interface NamedProblemPattern extends ProblemPattern { + export interface INamedProblemPattern extends IProblemPattern { /** * The name of the problem pattern. */ @@ -619,13 +619,13 @@ export namespace Config { } export namespace NamedProblemPattern { - export function is(value: any): value is NamedProblemPattern { - let candidate: NamedProblemPattern = value as NamedProblemPattern; + export function is(value: any): value is INamedProblemPattern { + const candidate: INamedProblemPattern = value as INamedProblemPattern; return candidate && Types.isString(candidate.name); } } - export interface NamedCheckedProblemPattern extends NamedProblemPattern { + export interface INamedCheckedProblemPattern extends INamedProblemPattern { /** * The regular expression to find a problem in the console output of an * executed task. @@ -634,13 +634,13 @@ export namespace Config { } export namespace NamedCheckedProblemPattern { - export function is(value: any): value is NamedCheckedProblemPattern { - let candidate: NamedProblemPattern = value as NamedProblemPattern; + export function is(value: any): value is INamedCheckedProblemPattern { + const candidate: INamedProblemPattern = value as INamedProblemPattern; return candidate && NamedProblemPattern.is(candidate) && Types.isString(candidate.regexp); } } - export type MultiLineProblemPattern = ProblemPattern[]; + export type MultiLineProblemPattern = IProblemPattern[]; export namespace MultiLineProblemPattern { export function is(value: any): value is MultiLineProblemPattern { @@ -648,7 +648,7 @@ export namespace Config { } } - export type MultiLineCheckedProblemPattern = CheckedProblemPattern[]; + export type MultiLineCheckedProblemPattern = ICheckedProblemPattern[]; export namespace MultiLineCheckedProblemPattern { export function is(value: any): value is MultiLineCheckedProblemPattern { @@ -664,7 +664,7 @@ export namespace Config { } } - export interface NamedMultiLineCheckedProblemPattern { + export interface INamedMultiLineCheckedProblemPattern { /** * The name of the problem pattern. */ @@ -682,18 +682,18 @@ export namespace Config { } export namespace NamedMultiLineCheckedProblemPattern { - export function is(value: any): value is NamedMultiLineCheckedProblemPattern { - let candidate = value as NamedMultiLineCheckedProblemPattern; + export function is(value: any): value is INamedMultiLineCheckedProblemPattern { + const candidate = value as INamedMultiLineCheckedProblemPattern; return candidate && Types.isString(candidate.name) && Types.isArray(candidate.patterns) && MultiLineCheckedProblemPattern.is(candidate.patterns); } } - export type NamedProblemPatterns = (Config.NamedProblemPattern | Config.NamedMultiLineCheckedProblemPattern)[]; + export type NamedProblemPatterns = (Config.INamedProblemPattern | Config.INamedMultiLineCheckedProblemPattern)[]; /** * A watching pattern */ - export interface WatchingPattern { + export interface IWatchingPattern { /** * The actual regular expression */ @@ -709,7 +709,7 @@ export namespace Config { /** * A description to track the start and end of a watching task. */ - export interface BackgroundMonitor { + export interface IBackgroundMonitor { /** * If set to true the watcher is in active mode when the task @@ -721,12 +721,12 @@ export namespace Config { /** * If matched in the output the start of a watching task is signaled. */ - beginsPattern?: string | WatchingPattern; + beginsPattern?: string | IWatchingPattern; /** * If matched in the output the end of a watching task is signaled. */ - endsPattern?: string | WatchingPattern; + endsPattern?: string | IWatchingPattern; } /** @@ -804,7 +804,7 @@ export namespace Config { * of a problem pattern or an array of problem patterns to match * problems spread over multiple lines. */ - pattern?: string | ProblemPattern | ProblemPattern[]; + pattern?: string | IProblemPattern | IProblemPattern[]; /** * A regular expression signaling that a watched tasks begins executing @@ -820,13 +820,13 @@ export namespace Config { /** * @deprecated Use background instead. */ - watching?: BackgroundMonitor; - background?: BackgroundMonitor; + watching?: IBackgroundMonitor; + background?: IBackgroundMonitor; } export type ProblemMatcherType = string | ProblemMatcher | Array<string | ProblemMatcher>; - export interface NamedProblemMatcher extends ProblemMatcher { + export interface INamedProblemMatcher extends ProblemMatcher { /** * This name can be used to refer to the * problem matcher from within a task. @@ -839,8 +839,8 @@ export namespace Config { label?: string; } - export function isNamedProblemMatcher(value: ProblemMatcher): value is NamedProblemMatcher { - return Types.isString((<NamedProblemMatcher>value).name); + export function isNamedProblemMatcher(value: ProblemMatcher): value is INamedProblemMatcher { + return Types.isString((<INamedProblemMatcher>value).name); } } @@ -850,17 +850,17 @@ export class ProblemPatternParser extends Parser { super(logger); } - public parse(value: Config.ProblemPattern): ProblemPattern; + public parse(value: Config.IProblemPattern): IProblemPattern; public parse(value: Config.MultiLineProblemPattern): MultiLineProblemPattern; - public parse(value: Config.NamedProblemPattern): NamedProblemPattern; - public parse(value: Config.NamedMultiLineCheckedProblemPattern): NamedMultiLineProblemPattern; - public parse(value: Config.ProblemPattern | Config.MultiLineProblemPattern | Config.NamedProblemPattern | Config.NamedMultiLineCheckedProblemPattern): any { + public parse(value: Config.INamedProblemPattern): INamedProblemPattern; + public parse(value: Config.INamedMultiLineCheckedProblemPattern): INamedMultiLineProblemPattern; + public parse(value: Config.IProblemPattern | Config.MultiLineProblemPattern | Config.INamedProblemPattern | Config.INamedMultiLineCheckedProblemPattern): any { if (Config.NamedMultiLineCheckedProblemPattern.is(value)) { return this.createNamedMultiLineProblemPattern(value); } else if (Config.MultiLineCheckedProblemPattern.is(value)) { return this.createMultiLineProblemPattern(value); } else if (Config.NamedCheckedProblemPattern.is(value)) { - let result = this.createSingleProblemPattern(value) as NamedProblemPattern; + const result = this.createSingleProblemPattern(value) as INamedProblemPattern; result.name = value.name; return result; } else if (Config.CheckedProblemPattern.is(value)) { @@ -871,8 +871,8 @@ export class ProblemPatternParser extends Parser { } } - private createSingleProblemPattern(value: Config.CheckedProblemPattern): ProblemPattern | null { - let result = this.doCreateSingleProblemPattern(value, true); + private createSingleProblemPattern(value: Config.ICheckedProblemPattern): IProblemPattern | null { + const result = this.doCreateSingleProblemPattern(value, true); if (result === undefined) { return null; } else if (result.kind === undefined) { @@ -881,12 +881,12 @@ export class ProblemPatternParser extends Parser { return this.validateProblemPattern([result]) ? result : null; } - private createNamedMultiLineProblemPattern(value: Config.NamedMultiLineCheckedProblemPattern): NamedMultiLineProblemPattern | null { + private createNamedMultiLineProblemPattern(value: Config.INamedMultiLineCheckedProblemPattern): INamedMultiLineProblemPattern | null { const validPatterns = this.createMultiLineProblemPattern(value.patterns); if (!validPatterns) { return null; } - let result = { + const result = { name: value.name, label: value.label ? value.label : value.name, patterns: validPatterns @@ -895,9 +895,9 @@ export class ProblemPatternParser extends Parser { } private createMultiLineProblemPattern(values: Config.MultiLineCheckedProblemPattern): MultiLineProblemPattern | null { - let result: MultiLineProblemPattern = []; + const result: MultiLineProblemPattern = []; for (let i = 0; i < values.length; i++) { - let pattern = this.doCreateSingleProblemPattern(values[i], false); + const pattern = this.doCreateSingleProblemPattern(values[i], false); if (pattern === undefined) { return null; } @@ -915,17 +915,17 @@ export class ProblemPatternParser extends Parser { return this.validateProblemPattern(result) ? result : null; } - private doCreateSingleProblemPattern(value: Config.CheckedProblemPattern, setDefaults: boolean): ProblemPattern | undefined { + private doCreateSingleProblemPattern(value: Config.ICheckedProblemPattern, setDefaults: boolean): IProblemPattern | undefined { const regexp = this.createRegularExpression(value.regexp); if (regexp === undefined) { return undefined; } - let result: ProblemPattern = { regexp }; + let result: IProblemPattern = { regexp }; if (value.kind) { result.kind = ProblemLocationKind.fromString(value.kind); } - function copyProperty(result: ProblemPattern, source: Config.ProblemPattern, resultKey: keyof ProblemPattern, sourceKey: keyof Config.ProblemPattern) { + function copyProperty(result: IProblemPattern, source: Config.IProblemPattern, resultKey: keyof IProblemPattern, sourceKey: keyof Config.IProblemPattern) { const value = source[sourceKey]; if (typeof value === 'number') { (result as any)[resultKey] = value; @@ -945,13 +945,13 @@ export class ProblemPatternParser extends Parser { } if (setDefaults) { if (result.location || result.kind === ProblemLocationKind.File) { - let defaultValue: Partial<ProblemPattern> = { + const defaultValue: Partial<IProblemPattern> = { file: 1, message: 0 }; result = Objects.mixin(result, defaultValue, false); } else { - let defaultValue: Partial<ProblemPattern> = { + const defaultValue: Partial<IProblemPattern> = { file: 1, line: 2, character: 3, @@ -963,9 +963,9 @@ export class ProblemPatternParser extends Parser { return result; } - private validateProblemPattern(values: ProblemPattern[]): boolean { + private validateProblemPattern(values: IProblemPattern[]): boolean { let file: boolean = false, message: boolean = false, location: boolean = false, line: boolean = false; - let locationKind = (values[0].kind === undefined) ? ProblemLocationKind.Location : values[0].kind; + const locationKind = (values[0].kind === undefined) ? ProblemLocationKind.Location : values[0].kind; values.forEach((pattern, i) => { if (i !== 0 && pattern.kind) { @@ -1136,12 +1136,12 @@ const problemPatternExtPoint = ExtensionsRegistry.registerExtensionPoint<Config. export interface IProblemPatternRegistry { onReady(): Promise<void>; - get(key: string): ProblemPattern | MultiLineProblemPattern; + get(key: string): IProblemPattern | MultiLineProblemPattern; } class ProblemPatternRegistryImpl implements IProblemPatternRegistry { - private patterns: IStringDictionary<ProblemPattern | ProblemPattern[]>; + private patterns: IStringDictionary<IProblemPattern | IProblemPattern[]>; private readyPromise: Promise<void>; constructor() { @@ -1152,19 +1152,19 @@ class ProblemPatternRegistryImpl implements IProblemPatternRegistry { // We get all statically know extension during startup in one batch try { delta.removed.forEach(extension => { - let problemPatterns = extension.value as Config.NamedProblemPatterns; - for (let pattern of problemPatterns) { + const problemPatterns = extension.value as Config.NamedProblemPatterns; + for (const pattern of problemPatterns) { if (this.patterns[pattern.name]) { delete this.patterns[pattern.name]; } } }); delta.added.forEach(extension => { - let problemPatterns = extension.value as Config.NamedProblemPatterns; - let parser = new ProblemPatternParser(new ExtensionRegistryReporter(extension.collector)); - for (let pattern of problemPatterns) { + const problemPatterns = extension.value as Config.NamedProblemPatterns; + const parser = new ProblemPatternParser(new ExtensionRegistryReporter(extension.collector)); + for (const pattern of problemPatterns) { if (Config.NamedMultiLineCheckedProblemPattern.is(pattern)) { - let result = parser.parse(pattern); + const result = parser.parse(pattern); if (parser.problemReporter.status.state < ValidationState.Error) { this.add(result.name, result.patterns); } else { @@ -1173,7 +1173,7 @@ class ProblemPatternRegistryImpl implements IProblemPatternRegistry { } } else if (Config.NamedProblemPattern.is(pattern)) { - let result = parser.parse(pattern); + const result = parser.parse(pattern); if (parser.problemReporter.status.state < ValidationState.Error) { this.add(pattern.name, result); } else { @@ -1196,11 +1196,11 @@ class ProblemPatternRegistryImpl implements IProblemPatternRegistry { return this.readyPromise; } - public add(key: string, value: ProblemPattern | ProblemPattern[]): void { + public add(key: string, value: IProblemPattern | IProblemPattern[]): void { this.patterns[key] = value; } - public get(key: string): ProblemPattern | ProblemPattern[] { + public get(key: string): IProblemPattern | IProblemPattern[] { return this.patterns[key]; } @@ -1328,7 +1328,7 @@ export class ProblemMatcherParser extends Parser { } public parse(json: Config.ProblemMatcher): ProblemMatcher | undefined { - let result = this.createProblemMatcher(json); + const result = this.createProblemMatcher(json); if (!this.checkProblemMatcherValid(json, result)) { return undefined; } @@ -1360,8 +1360,8 @@ export class ProblemMatcherParser extends Parser { private createProblemMatcher(description: Config.ProblemMatcher): ProblemMatcher | null { let result: ProblemMatcher | null = null; - let owner = Types.isString(description.owner) ? description.owner : UUID.generateUuid(); - let source = Types.isString(description.source) ? description.source : undefined; + const owner = Types.isString(description.owner) ? description.owner : UUID.generateUuid(); + const source = Types.isString(description.source) ? description.source : undefined; let applyTo = Types.isString(description.applyTo) ? ApplyToKind.fromString(description.applyTo) : ApplyToKind.allDocuments; if (!applyTo) { applyTo = ApplyToKind.allDocuments; @@ -1382,7 +1382,7 @@ export class ProblemMatcherParser extends Parser { } } } else if (Types.isStringArray(description.fileLocation)) { - let values = <string[]>description.fileLocation; + const values = <string[]>description.fileLocation; if (values.length > 0) { kind = FileLocationKind.fromString(values[0]); if (values.length === 1 && kind === FileLocationKind.Absolute) { @@ -1394,7 +1394,7 @@ export class ProblemMatcherParser extends Parser { } } - let pattern = description.pattern ? this.createProblemPattern(description.pattern) : undefined; + const pattern = description.pattern ? this.createProblemPattern(description.pattern) : undefined; let severity = description.severity ? Severity.fromValue(description.severity) : undefined; if (severity === Severity.Ignore) { @@ -1403,9 +1403,9 @@ export class ProblemMatcherParser extends Parser { } if (Types.isString(description.base)) { - let variableName = <string>description.base; + const variableName = <string>description.base; if (variableName.length > 1 && variableName[0] === '$') { - let base = ProblemMatcherRegistry.get(variableName.substring(1)); + const base = ProblemMatcherRegistry.get(variableName.substring(1)); if (base) { result = Objects.deepClone(base); if (description.owner !== undefined && owner !== undefined) { @@ -1447,17 +1447,17 @@ export class ProblemMatcherParser extends Parser { } } if (Config.isNamedProblemMatcher(description)) { - (result as NamedProblemMatcher).name = description.name; - (result as NamedProblemMatcher).label = Types.isString(description.label) ? description.label : description.name; + (result as INamedProblemMatcher).name = description.name; + (result as INamedProblemMatcher).label = Types.isString(description.label) ? description.label : description.name; } return result; } - private createProblemPattern(value: string | Config.ProblemPattern | Config.MultiLineProblemPattern): ProblemPattern | ProblemPattern[] | null { + private createProblemPattern(value: string | Config.IProblemPattern | Config.MultiLineProblemPattern): IProblemPattern | IProblemPattern[] | null { if (Types.isString(value)) { - let variableName: string = <string>value; + const variableName: string = <string>value; if (variableName.length > 1 && variableName[0] === '$') { - let result = ProblemPatternRegistry.get(variableName.substring(1)); + const result = ProblemPatternRegistry.get(variableName.substring(1)); if (!result) { this.error(localize('ProblemMatcherParser.noDefinedPatter', 'Error: the pattern with the identifier {0} doesn\'t exist.', variableName)); } @@ -1470,7 +1470,7 @@ export class ProblemMatcherParser extends Parser { } } } else if (value) { - let problemPatternParser = new ProblemPatternParser(this.problemReporter); + const problemPatternParser = new ProblemPatternParser(this.problemReporter); if (Array.isArray(value)) { return problemPatternParser.parse(value); } else { @@ -1481,8 +1481,8 @@ export class ProblemMatcherParser extends Parser { } private addWatchingMatcher(external: Config.ProblemMatcher, internal: ProblemMatcher): void { - let oldBegins = this.createRegularExpression(external.watchedTaskBeginsRegExp); - let oldEnds = this.createRegularExpression(external.watchedTaskEndsRegExp); + const oldBegins = this.createRegularExpression(external.watchedTaskBeginsRegExp); + const oldEnds = this.createRegularExpression(external.watchedTaskEndsRegExp); if (oldBegins && oldEnds) { internal.watching = { activeOnStart: false, @@ -1491,12 +1491,12 @@ export class ProblemMatcherParser extends Parser { }; return; } - let backgroundMonitor = external.background || external.watching; + const backgroundMonitor = external.background || external.watching; if (Types.isUndefinedOrNull(backgroundMonitor)) { return; } - let begins: WatchingPattern | null = this.createWatchingPattern(backgroundMonitor.beginsPattern); - let ends: WatchingPattern | null = this.createWatchingPattern(backgroundMonitor.endsPattern); + const begins: IWatchingPattern | null = this.createWatchingPattern(backgroundMonitor.beginsPattern); + const ends: IWatchingPattern | null = this.createWatchingPattern(backgroundMonitor.endsPattern); if (begins && ends) { internal.watching = { activeOnStart: Types.isBoolean(backgroundMonitor.activeOnStart) ? backgroundMonitor.activeOnStart : false, @@ -1510,7 +1510,7 @@ export class ProblemMatcherParser extends Parser { } } - private createWatchingPattern(external: string | Config.WatchingPattern | undefined): WatchingPattern | null { + private createWatchingPattern(external: string | Config.IWatchingPattern | undefined): IWatchingPattern | null { if (Types.isUndefinedOrNull(external)) { return null; } @@ -1703,7 +1703,7 @@ export namespace Schemas { }; } -const problemMatchersExtPoint = ExtensionsRegistry.registerExtensionPoint<Config.NamedProblemMatcher[]>({ +const problemMatchersExtPoint = ExtensionsRegistry.registerExtensionPoint<Config.INamedProblemMatcher[]>({ extensionPoint: 'problemMatchers', deps: [problemPatternExtPoint], jsonSchema: { @@ -1715,14 +1715,14 @@ const problemMatchersExtPoint = ExtensionsRegistry.registerExtensionPoint<Config export interface IProblemMatcherRegistry { onReady(): Promise<void>; - get(name: string): NamedProblemMatcher; + get(name: string): INamedProblemMatcher; keys(): string[]; readonly onMatcherChanged: Event<void>; } class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { - private matchers: IStringDictionary<NamedProblemMatcher>; + private matchers: IStringDictionary<INamedProblemMatcher>; private readyPromise: Promise<void>; private readonly _onMatchersChanged: Emitter<void> = new Emitter<void>(); public readonly onMatcherChanged: Event<void> = this._onMatchersChanged.event; @@ -1735,18 +1735,18 @@ class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { problemMatchersExtPoint.setHandler((extensions, delta) => { try { delta.removed.forEach(extension => { - let problemMatchers = extension.value; - for (let matcher of problemMatchers) { + const problemMatchers = extension.value; + for (const matcher of problemMatchers) { if (this.matchers[matcher.name]) { delete this.matchers[matcher.name]; } } }); delta.added.forEach(extension => { - let problemMatchers = extension.value; - let parser = new ProblemMatcherParser(new ExtensionRegistryReporter(extension.collector)); - for (let matcher of problemMatchers) { - let result = parser.parse(matcher); + const problemMatchers = extension.value; + const parser = new ProblemMatcherParser(new ExtensionRegistryReporter(extension.collector)); + for (const matcher of problemMatchers) { + const result = parser.parse(matcher); if (result && isNamedProblemMatcher(result)) { this.add(result); } @@ -1757,7 +1757,7 @@ class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { } } catch (error) { } - let matcher = this.get('tsc-watch'); + const matcher = this.get('tsc-watch'); if (matcher) { (<any>matcher).tscWatch = true; } @@ -1771,11 +1771,11 @@ class ProblemMatcherRegistryImpl implements IProblemMatcherRegistry { return this.readyPromise; } - public add(matcher: NamedProblemMatcher): void { + public add(matcher: INamedProblemMatcher): void { this.matchers[matcher.name] = matcher; } - public get(name: string): NamedProblemMatcher { + public get(name: string): INamedProblemMatcher { return this.matchers[name]; } diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 9c6f0ba9e34..c5d4058bcb0 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -14,16 +14,16 @@ import * as UUID from 'vs/base/common/uuid'; import { ValidationStatus, IProblemReporter as IProblemReporterBase } from 'vs/base/common/parsers'; import { - NamedProblemMatcher, ProblemMatcher, ProblemMatcherParser, Config as ProblemMatcherConfig, - isNamedProblemMatcher, ProblemMatcherRegistry + INamedProblemMatcher, ProblemMatcherParser, Config as ProblemMatcherConfig, + isNamedProblemMatcher, ProblemMatcherRegistry, ProblemMatcher } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; import * as Tasks from './tasks'; -import { TaskDefinitionRegistry } from './taskDefinitionRegistry'; +import { ITaskDefinitionRegistry, TaskDefinitionRegistry } from './taskDefinitionRegistry'; import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { URI } from 'vs/base/common/uri'; -import { USER_TASKS_GROUP_KEY, ShellExecutionSupportedContext, ProcessExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; +import { ShellExecutionSupportedContext, ProcessExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const enum ShellQuoting { @@ -43,7 +43,7 @@ export const enum ShellQuoting { weak = 3 } -export interface ShellQuotingOptions { +export interface IShellQuotingOptions { /** * The character used to do character escaping. */ @@ -63,13 +63,13 @@ export interface ShellQuotingOptions { weak?: string; } -export interface ShellConfiguration { +export interface IShellConfiguration { executable?: string; args?: string[]; - quoting?: ShellQuotingOptions; + quoting?: IShellQuotingOptions; } -export interface CommandOptionsConfig { +export interface ICommandOptionsConfig { /** * The current working directory of the executed program or shell. * If omitted VSCode's current workspace root is used. @@ -85,10 +85,10 @@ export interface CommandOptionsConfig { /** * The shell configuration; */ - shell?: ShellConfiguration; + shell?: IShellConfiguration; } -export interface PresentationOptionsConfig { +export interface IPresentationOptionsConfig { /** * Controls whether the terminal executing a task is brought to front or not. * Defaults to `RevealKind.Always`. @@ -137,25 +137,25 @@ export interface PresentationOptionsConfig { close?: boolean; } -export interface RunOptionsConfig { +export interface IRunOptionsConfig { reevaluateOnRerun?: boolean; runOn?: string; instanceLimit?: number; } -export interface TaskIdentifier { +export interface ITaskIdentifier { type?: string; [name: string]: any; } -export namespace TaskIdentifier { - export function is(value: any): value is TaskIdentifier { - let candidate: TaskIdentifier = value; +export namespace ITaskIdentifier { + export function is(value: any): value is ITaskIdentifier { + const candidate: ITaskIdentifier = value; return candidate !== undefined && Types.isString(value.type); } } -export interface LegacyTaskProperties { +export interface ILegacyTaskProperties { /** * @deprecated Use `isBackground` instead. * Whether the executed command is kept alive and is watching the file system. @@ -175,7 +175,7 @@ export interface LegacyTaskProperties { isTestCommand?: boolean; } -export interface LegacyCommandProperties { +export interface ILegacyCommandProperties { /** * Whether this is a shell or process @@ -198,7 +198,7 @@ export interface LegacyCommandProperties { /** * @deprecated Use presentation instead */ - terminal?: PresentationOptionsConfig; + terminal?: IPresentationOptionsConfig; /** * @deprecated Use inline commands. @@ -220,7 +220,7 @@ export interface LegacyCommandProperties { * * Defaults to false if omitted. */ - isShellCommand?: boolean | ShellConfiguration; + isShellCommand?: boolean | IShellConfiguration; } export type CommandString = string | string[] | { value: string | string[]; quoting: 'escape' | 'strong' | 'weak' }; @@ -241,7 +241,7 @@ export namespace CommandString { } } -export interface BaseCommandProperties { +export interface IBaseCommandProperties { /** * The command to be executed. Can be an external program or a shell @@ -252,7 +252,7 @@ export interface BaseCommandProperties { /** * The command options used when the command is executed. Can be omitted. */ - options?: CommandOptionsConfig; + options?: ICommandOptionsConfig; /** * The arguments passed to the command or additional arguments passed to the @@ -262,30 +262,30 @@ export interface BaseCommandProperties { } -export interface CommandProperties extends BaseCommandProperties { +export interface ICommandProperties extends IBaseCommandProperties { /** * Windows specific command properties */ - windows?: BaseCommandProperties; + windows?: IBaseCommandProperties; /** * OSX specific command properties */ - osx?: BaseCommandProperties; + osx?: IBaseCommandProperties; /** * linux specific command properties */ - linux?: BaseCommandProperties; + linux?: IBaseCommandProperties; } -export interface GroupKind { +export interface IGroupKind { kind?: string; isDefault?: boolean | string; } -export interface ConfigurationProperties { +export interface IConfigurationProperties { /** * The task's name */ @@ -315,7 +315,7 @@ export interface ConfigurationProperties { /** * Defines the group the task belongs too. */ - group?: string | GroupKind; + group?: string | IGroupKind; /** * A description of the task. @@ -325,7 +325,7 @@ export interface ConfigurationProperties { /** * The other tasks the task depend on */ - dependsOn?: string | TaskIdentifier | Array<string | TaskIdentifier>; + dependsOn?: string | ITaskIdentifier | Array<string | ITaskIdentifier>; /** * The order the dependsOn tasks should be executed in. @@ -335,12 +335,12 @@ export interface ConfigurationProperties { /** * Controls the behavior of the used terminal */ - presentation?: PresentationOptionsConfig; + presentation?: IPresentationOptionsConfig; /** * Controls shell options. */ - options?: CommandOptionsConfig; + options?: ICommandOptionsConfig; /** * The problem matcher(s) to use to capture problems in the tasks @@ -351,10 +351,20 @@ export interface ConfigurationProperties { /** * Task run options. Control run related properties. */ - runOptions?: RunOptionsConfig; + runOptions?: IRunOptionsConfig; + + /** + * The icon for this task in the terminal tabs list + */ + icon?: { id: string; color?: string }; + + /** + * The icon's color in the terminal tabs list + */ + color?: string; } -export interface CustomTask extends CommandProperties, ConfigurationProperties { +export interface ICustomTask extends ICommandProperties, IConfigurationProperties { /** * Custom tasks have the type CUSTOMIZED_TASK_TYPE */ @@ -362,7 +372,7 @@ export interface CustomTask extends CommandProperties, ConfigurationProperties { } -export interface ConfiguringTask extends ConfigurationProperties { +export interface IConfiguringTask extends IConfigurationProperties { /** * The contributed type of the task */ @@ -372,7 +382,7 @@ export interface ConfiguringTask extends ConfigurationProperties { /** * The base task runner configuration */ -export interface BaseTaskRunnerConfiguration { +export interface IBaseTaskRunnerConfiguration { /** * The command to be executed. Can be an external program or a shell @@ -398,7 +408,7 @@ export interface BaseTaskRunnerConfiguration { /** * The command options used when the command is executed. Can be omitted. */ - options?: CommandOptionsConfig; + options?: ICommandOptionsConfig; /** * The arguments passed to the command. Can be omitted. @@ -424,12 +434,12 @@ export interface BaseTaskRunnerConfiguration { /** * The group */ - group?: string | GroupKind; + group?: string | IGroupKind; /** * Controls the behavior of the used terminal */ - presentation?: PresentationOptionsConfig; + presentation?: IPresentationOptionsConfig; /** * If set to false the task name is added as an additional argument to the @@ -475,12 +485,12 @@ export interface BaseTaskRunnerConfiguration { * The configuration of the available tasks. A tasks.json file can either * contain a global problemMatcher property or a tasks property but not both. */ - tasks?: Array<CustomTask | ConfiguringTask>; + tasks?: Array<ICustomTask | IConfiguringTask>; /** * Problem matcher declarations. */ - declares?: ProblemMatcherConfig.NamedProblemMatcher[]; + declares?: ProblemMatcherConfig.INamedProblemMatcher[]; /** * Optional user input variables. @@ -492,7 +502,7 @@ export interface BaseTaskRunnerConfiguration { * A configuration of an external build system. BuildConfiguration.buildSystem * must be set to 'program' */ -export interface ExternalTaskRunnerConfiguration extends BaseTaskRunnerConfiguration { +export interface IExternalTaskRunnerConfiguration extends IBaseTaskRunnerConfiguration { _runner?: string; @@ -509,17 +519,17 @@ export interface ExternalTaskRunnerConfiguration extends BaseTaskRunnerConfigura /** * Windows specific task configuration */ - windows?: BaseTaskRunnerConfiguration; + windows?: IBaseTaskRunnerConfiguration; /** * Mac specific task configuration */ - osx?: BaseTaskRunnerConfiguration; + osx?: IBaseTaskRunnerConfiguration; /** * Linux specific task configuration */ - linux?: BaseTaskRunnerConfiguration; + linux?: IBaseTaskRunnerConfiguration; } enum ProblemMatcherKind { @@ -552,26 +562,26 @@ function fillProperty<T, K extends keyof T>(target: T, source: Partial<T>, key: } -interface ParserType<T> { +interface IParserType<T> { isEmpty(value: T | undefined): boolean; assignProperties(target: T | undefined, source: T | undefined): T | undefined; fillProperties(target: T | undefined, source: T | undefined): T | undefined; - fillDefaults(value: T | undefined, context: ParseContext): T | undefined; + fillDefaults(value: T | undefined, context: IParseContext): T | undefined; freeze(value: T): Readonly<T> | undefined; } -interface MetaData<T, U> { +interface IMetaData<T, U> { property: keyof T; - type?: ParserType<U>; + type?: IParserType<U>; } -function _isEmpty<T>(this: void, value: T | undefined, properties: MetaData<T, any>[] | undefined, allowEmptyArray: boolean = false): boolean { +function _isEmpty<T>(this: void, value: T | undefined, properties: IMetaData<T, any>[] | undefined, allowEmptyArray: boolean = false): boolean { if (value === undefined || value === null || properties === undefined) { return true; } - for (let meta of properties) { - let property = value[meta.property]; + for (const meta of properties) { + const property = value[meta.property]; if (property !== undefined && property !== null) { if (meta.type !== undefined && !meta.type.isEmpty(property)) { return false; @@ -583,15 +593,15 @@ function _isEmpty<T>(this: void, value: T | undefined, properties: MetaData<T, a return true; } -function _assignProperties<T>(this: void, target: T | undefined, source: T | undefined, properties: MetaData<T, any>[]): T | undefined { +function _assignProperties<T>(this: void, target: T | undefined, source: T | undefined, properties: IMetaData<T, any>[]): T | undefined { if (!source || _isEmpty(source, properties)) { return target; } if (!target || _isEmpty(target, properties)) { return source; } - for (let meta of properties) { - let property = meta.property; + for (const meta of properties) { + const property = meta.property; let value: any; if (meta.type !== undefined) { value = meta.type.assignProperties(target[property], source[property]); @@ -605,15 +615,15 @@ function _assignProperties<T>(this: void, target: T | undefined, source: T | und return target; } -function _fillProperties<T>(this: void, target: T | undefined, source: T | undefined, properties: MetaData<T, any>[] | undefined, allowEmptyArray: boolean = false): T | undefined { +function _fillProperties<T>(this: void, target: T | undefined, source: T | undefined, properties: IMetaData<T, any>[] | undefined, allowEmptyArray: boolean = false): T | undefined { if (!source || _isEmpty(source, properties)) { return target; } if (!target || _isEmpty(target, properties, allowEmptyArray)) { return source; } - for (let meta of properties!) { - let property = meta.property; + for (const meta of properties!) { + const property = meta.property; let value: any; if (meta.type) { value = meta.type.fillProperties(target[property], source[property]); @@ -627,7 +637,7 @@ function _fillProperties<T>(this: void, target: T | undefined, source: T | undef return target; } -function _fillDefaults<T>(this: void, target: T | undefined, defaults: T | undefined, properties: MetaData<T, any>[], context: ParseContext): T | undefined { +function _fillDefaults<T>(this: void, target: T | undefined, defaults: T | undefined, properties: IMetaData<T, any>[], context: IParseContext): T | undefined { if (target && Object.isFrozen(target)) { return target; } @@ -638,8 +648,8 @@ function _fillDefaults<T>(this: void, target: T | undefined, defaults: T | undef return undefined; } } - for (let meta of properties) { - let property = meta.property; + for (const meta of properties) { + const property = meta.property; if (target[property] !== undefined) { continue; } @@ -657,16 +667,16 @@ function _fillDefaults<T>(this: void, target: T | undefined, defaults: T | undef return target; } -function _freeze<T>(this: void, target: T, properties: MetaData<T, any>[]): Readonly<T> | undefined { +function _freeze<T>(this: void, target: T, properties: IMetaData<T, any>[]): Readonly<T> | undefined { if (target === undefined || target === null) { return undefined; } if (Object.isFrozen(target)) { return target; } - for (let meta of properties) { + for (const meta of properties) { if (meta.type) { - let value = target[meta.property]; + const value = target[meta.property]; if (value) { meta.type.freeze(value); } @@ -692,8 +702,8 @@ export namespace RunOnOptions { } export namespace RunOptions { - const properties: MetaData<Tasks.RunOptions, void>[] = [{ property: 'reevaluateOnRerun' }, { property: 'runOn' }, { property: 'instanceLimit' }]; - export function fromConfiguration(value: RunOptionsConfig | undefined): Tasks.RunOptions { + const properties: IMetaData<Tasks.IRunOptions, void>[] = [{ property: 'reevaluateOnRerun' }, { property: 'runOn' }, { property: 'instanceLimit' }]; + export function fromConfiguration(value: IRunOptionsConfig | undefined): Tasks.IRunOptions { return { reevaluateOnRerun: value ? value.reevaluateOnRerun : true, runOn: value ? RunOnOptions.fromString(value.runOn) : Tasks.RunOnOptions.default, @@ -701,20 +711,20 @@ export namespace RunOptions { }; } - export function assignProperties(target: Tasks.RunOptions, source: Tasks.RunOptions | undefined): Tasks.RunOptions { + export function assignProperties(target: Tasks.IRunOptions, source: Tasks.IRunOptions | undefined): Tasks.IRunOptions { return _assignProperties(target, source, properties)!; } - export function fillProperties(target: Tasks.RunOptions, source: Tasks.RunOptions | undefined): Tasks.RunOptions { + export function fillProperties(target: Tasks.IRunOptions, source: Tasks.IRunOptions | undefined): Tasks.IRunOptions { return _fillProperties(target, source, properties)!; } } -interface ParseContext { +export interface IParseContext { workspaceFolder: IWorkspaceFolder; workspace: IWorkspace | undefined; problemReporter: IProblemReporter; - namedProblemMatchers: IStringDictionary<NamedProblemMatcher>; + namedProblemMatchers: IStringDictionary<INamedProblemMatcher>; uuidMap: UUIDMap; engine: Tasks.ExecutionEngine; schemaVersion: Tasks.JsonSchemaVersion; @@ -726,18 +736,18 @@ interface ParseContext { namespace ShellConfiguration { - const properties: MetaData<Tasks.ShellConfiguration, void>[] = [{ property: 'executable' }, { property: 'args' }, { property: 'quoting' }]; + const properties: IMetaData<Tasks.IShellConfiguration, void>[] = [{ property: 'executable' }, { property: 'args' }, { property: 'quoting' }]; - export function is(value: any): value is ShellConfiguration { - let candidate: ShellConfiguration = value; + export function is(value: any): value is IShellConfiguration { + const candidate: IShellConfiguration = value; return candidate && (Types.isString(candidate.executable) || Types.isStringArray(candidate.args)); } - export function from(this: void, config: ShellConfiguration | undefined, context: ParseContext): Tasks.ShellConfiguration | undefined { + export function from(this: void, config: IShellConfiguration | undefined, context: IParseContext): Tasks.IShellConfiguration | undefined { if (!is(config)) { return undefined; } - let result: ShellConfiguration = {}; + const result: IShellConfiguration = {}; if (config.executable !== undefined) { result.executable = config.executable; } @@ -751,23 +761,23 @@ namespace ShellConfiguration { return result; } - export function isEmpty(this: void, value: Tasks.ShellConfiguration): boolean { + export function isEmpty(this: void, value: Tasks.IShellConfiguration): boolean { return _isEmpty(value, properties, true); } - export function assignProperties(this: void, target: Tasks.ShellConfiguration | undefined, source: Tasks.ShellConfiguration | undefined): Tasks.ShellConfiguration | undefined { + export function assignProperties(this: void, target: Tasks.IShellConfiguration | undefined, source: Tasks.IShellConfiguration | undefined): Tasks.IShellConfiguration | undefined { return _assignProperties(target, source, properties); } - export function fillProperties(this: void, target: Tasks.ShellConfiguration, source: Tasks.ShellConfiguration): Tasks.ShellConfiguration | undefined { + export function fillProperties(this: void, target: Tasks.IShellConfiguration, source: Tasks.IShellConfiguration): Tasks.IShellConfiguration | undefined { return _fillProperties(target, source, properties, true); } - export function fillDefaults(this: void, value: Tasks.ShellConfiguration, context: ParseContext): Tasks.ShellConfiguration { + export function fillDefaults(this: void, value: Tasks.IShellConfiguration, context: IParseContext): Tasks.IShellConfiguration { return value; } - export function freeze(this: void, value: Tasks.ShellConfiguration): Readonly<Tasks.ShellConfiguration> | undefined { + export function freeze(this: void, value: Tasks.IShellConfiguration): Readonly<Tasks.IShellConfiguration> | undefined { if (!value) { return undefined; } @@ -777,11 +787,11 @@ namespace ShellConfiguration { namespace CommandOptions { - const properties: MetaData<Tasks.CommandOptions, Tasks.ShellConfiguration>[] = [{ property: 'cwd' }, { property: 'env' }, { property: 'shell', type: ShellConfiguration }]; - const defaults: CommandOptionsConfig = { cwd: '${workspaceFolder}' }; + const properties: IMetaData<Tasks.CommandOptions, Tasks.IShellConfiguration>[] = [{ property: 'cwd' }, { property: 'env' }, { property: 'shell', type: ShellConfiguration }]; + const defaults: ICommandOptionsConfig = { cwd: '${workspaceFolder}' }; - export function from(this: void, options: CommandOptionsConfig, context: ParseContext): Tasks.CommandOptions | undefined { - let result: Tasks.CommandOptions = {}; + export function from(this: void, options: ICommandOptionsConfig, context: IParseContext): Tasks.CommandOptions | undefined { + const result: Tasks.CommandOptions = {}; if (options.cwd !== undefined) { if (Types.isString(options.cwd)) { result.cwd = options.cwd; @@ -811,7 +821,7 @@ namespace CommandOptions { if (target.env === undefined) { target.env = source.env; } else if (source.env !== undefined) { - let env: { [key: string]: string } = Object.create(null); + const env: { [key: string]: string } = Object.create(null); if (target.env !== undefined) { Object.keys(target.env).forEach(key => env[key] = target.env![key]); } @@ -828,7 +838,7 @@ namespace CommandOptions { return _fillProperties(target, source, properties); } - export function fillDefaults(value: Tasks.CommandOptions | undefined, context: ParseContext): Tasks.CommandOptions | undefined { + export function fillDefaults(value: Tasks.CommandOptions | undefined, context: IParseContext): Tasks.CommandOptions | undefined { return _fillDefaults(value, defaults, properties, context); } @@ -840,13 +850,13 @@ namespace CommandOptions { namespace CommandConfiguration { export namespace PresentationOptions { - const properties: MetaData<Tasks.PresentationOptions, void>[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'revealProblems' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }, { property: 'group' }, { property: 'close' }]; + const properties: IMetaData<Tasks.IPresentationOptions, void>[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'revealProblems' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }, { property: 'group' }, { property: 'close' }]; - interface PresentationOptionsShape extends LegacyCommandProperties { - presentation?: PresentationOptionsConfig; + interface IPresentationOptionsShape extends ILegacyCommandProperties { + presentation?: IPresentationOptionsConfig; } - export function from(this: void, config: PresentationOptionsShape, context: ParseContext): Tasks.PresentationOptions | undefined { + export function from(this: void, config: IPresentationOptionsShape, context: IParseContext): Tasks.IPresentationOptions | undefined { let echo: boolean; let reveal: Tasks.RevealKind; let revealProblems: Tasks.RevealProblemKind; @@ -865,7 +875,7 @@ namespace CommandConfiguration { reveal = Tasks.RevealKind.fromString(config.showOutput); hasProps = true; } - let presentation = config.presentation || config.terminal; + const presentation = config.presentation || config.terminal; if (presentation) { if (Types.isBoolean(presentation.echo)) { echo = presentation.echo; @@ -902,24 +912,24 @@ namespace CommandConfiguration { return { echo: echo!, reveal: reveal!, revealProblems: revealProblems!, focus: focus!, panel: panel!, showReuseMessage: showReuseMessage!, clear: clear!, group, close: close }; } - export function assignProperties(target: Tasks.PresentationOptions, source: Tasks.PresentationOptions | undefined): Tasks.PresentationOptions | undefined { + export function assignProperties(target: Tasks.IPresentationOptions, source: Tasks.IPresentationOptions | undefined): Tasks.IPresentationOptions | undefined { return _assignProperties(target, source, properties); } - export function fillProperties(target: Tasks.PresentationOptions, source: Tasks.PresentationOptions | undefined): Tasks.PresentationOptions | undefined { + export function fillProperties(target: Tasks.IPresentationOptions, source: Tasks.IPresentationOptions | undefined): Tasks.IPresentationOptions | undefined { return _fillProperties(target, source, properties); } - export function fillDefaults(value: Tasks.PresentationOptions, context: ParseContext): Tasks.PresentationOptions | undefined { - let defaultEcho = context.engine === Tasks.ExecutionEngine.Terminal ? true : false; + export function fillDefaults(value: Tasks.IPresentationOptions, context: IParseContext): Tasks.IPresentationOptions | undefined { + const defaultEcho = context.engine === Tasks.ExecutionEngine.Terminal ? true : false; return _fillDefaults(value, { echo: defaultEcho, reveal: Tasks.RevealKind.Always, revealProblems: Tasks.RevealProblemKind.Never, focus: false, panel: Tasks.PanelKind.Shared, showReuseMessage: true, clear: false }, properties, context); } - export function freeze(value: Tasks.PresentationOptions): Readonly<Tasks.PresentationOptions> | undefined { + export function freeze(value: Tasks.IPresentationOptions): Readonly<Tasks.IPresentationOptions> | undefined { return _freeze(value, properties); } - export function isEmpty(this: void, value: Tasks.PresentationOptions): boolean { + export function isEmpty(this: void, value: Tasks.IPresentationOptions): boolean { return _isEmpty(value, properties); } } @@ -934,8 +944,8 @@ namespace CommandConfiguration { } else if (Types.isStringArray(value)) { return value.join(' '); } else { - let quoting = Tasks.ShellQuoting.from(value.quoting); - let result = Types.isString(value.value) ? value.value : Types.isStringArray(value.value) ? value.value.join(' ') : undefined; + const quoting = Tasks.ShellQuoting.from(value.quoting); + const result = Types.isString(value.value) ? value.value : Types.isStringArray(value.value) ? value.value.join(' ') : undefined; if (result) { return { value: result, @@ -948,25 +958,25 @@ namespace CommandConfiguration { } } - interface BaseCommandConfigurationShape extends BaseCommandProperties, LegacyCommandProperties { + interface IBaseCommandConfigurationShape extends IBaseCommandProperties, ILegacyCommandProperties { } - interface CommandConfigurationShape extends BaseCommandConfigurationShape { - windows?: BaseCommandConfigurationShape; - osx?: BaseCommandConfigurationShape; - linux?: BaseCommandConfigurationShape; + interface ICommandConfigurationShape extends IBaseCommandConfigurationShape { + windows?: IBaseCommandConfigurationShape; + osx?: IBaseCommandConfigurationShape; + linux?: IBaseCommandConfigurationShape; } - const properties: MetaData<Tasks.CommandConfiguration, any>[] = [ + const properties: IMetaData<Tasks.ICommandConfiguration, any>[] = [ { property: 'runtime' }, { property: 'name' }, { property: 'options', type: CommandOptions }, { property: 'args' }, { property: 'taskSelector' }, { property: 'suppressTaskName' }, { property: 'presentation', type: PresentationOptions } ]; - export function from(this: void, config: CommandConfigurationShape, context: ParseContext): Tasks.CommandConfiguration | undefined { - let result: Tasks.CommandConfiguration = fromBase(config, context)!; + export function from(this: void, config: ICommandConfigurationShape, context: IParseContext): Tasks.ICommandConfiguration | undefined { + let result: Tasks.ICommandConfiguration = fromBase(config, context)!; - let osConfig: Tasks.CommandConfiguration | undefined = undefined; + let osConfig: Tasks.ICommandConfiguration | undefined = undefined; if (config.windows && context.platform === Platform.Windows) { osConfig = fromBase(config.windows, context); } else if (config.osx && context.platform === Platform.Mac) { @@ -980,22 +990,22 @@ namespace CommandConfiguration { return isEmpty(result) ? undefined : result; } - function fromBase(this: void, config: BaseCommandConfigurationShape, context: ParseContext): Tasks.CommandConfiguration | undefined { - let name: Tasks.CommandString | undefined = ShellString.from(config.command); + function fromBase(this: void, config: IBaseCommandConfigurationShape, context: IParseContext): Tasks.ICommandConfiguration | undefined { + const name: Tasks.CommandString | undefined = ShellString.from(config.command); let runtime: Tasks.RuntimeType; if (Types.isString(config.type)) { if (config.type === 'shell' || config.type === 'process') { runtime = Tasks.RuntimeType.fromString(config.type); } } - let isShellConfiguration = ShellConfiguration.is(config.isShellCommand); + const isShellConfiguration = ShellConfiguration.is(config.isShellCommand); if (Types.isBoolean(config.isShellCommand) || isShellConfiguration) { runtime = Tasks.RuntimeType.Shell; } else if (config.isShellCommand !== undefined) { runtime = !!config.isShellCommand ? Tasks.RuntimeType.Shell : Tasks.RuntimeType.Process; } - let result: Tasks.CommandConfiguration = { + const result: Tasks.ICommandConfiguration = { name: name, runtime: runtime!, presentation: PresentationOptions.from(config, context)! @@ -1003,8 +1013,8 @@ namespace CommandConfiguration { if (config.args !== undefined) { result.args = []; - for (let arg of config.args) { - let converted = ShellString.from(arg); + for (const arg of config.args) { + const converted = ShellString.from(arg); if (converted !== undefined) { result.args.push(converted); } else { @@ -1020,7 +1030,7 @@ namespace CommandConfiguration { if (config.options !== undefined) { result.options = CommandOptions.from(config.options, context); if (result.options && result.options.shell === undefined && isShellConfiguration) { - result.options.shell = ShellConfiguration.from(config.isShellCommand as ShellConfiguration, context); + result.options.shell = ShellConfiguration.from(config.isShellCommand as IShellConfiguration, context); if (context.engine !== Tasks.ExecutionEngine.Terminal) { context.taskLoadIssues.push(nls.localize('ConfigurationParser.noShell', 'Warning: shell configuration is only supported when executing tasks in the terminal.')); } @@ -1037,15 +1047,15 @@ namespace CommandConfiguration { return isEmpty(result) ? undefined : result; } - export function hasCommand(value: Tasks.CommandConfiguration): boolean { + export function hasCommand(value: Tasks.ICommandConfiguration): boolean { return value && !!value.name; } - export function isEmpty(value: Tasks.CommandConfiguration | undefined): boolean { + export function isEmpty(value: Tasks.ICommandConfiguration | undefined): boolean { return _isEmpty(value, properties); } - export function assignProperties(target: Tasks.CommandConfiguration, source: Tasks.CommandConfiguration, overwriteArgs: boolean): Tasks.CommandConfiguration { + export function assignProperties(target: Tasks.ICommandConfiguration, source: Tasks.ICommandConfiguration, overwriteArgs: boolean): Tasks.ICommandConfiguration { if (isEmpty(source)) { return target; } @@ -1068,11 +1078,11 @@ namespace CommandConfiguration { return target; } - export function fillProperties(target: Tasks.CommandConfiguration, source: Tasks.CommandConfiguration): Tasks.CommandConfiguration | undefined { + export function fillProperties(target: Tasks.ICommandConfiguration, source: Tasks.ICommandConfiguration): Tasks.ICommandConfiguration | undefined { return _fillProperties(target, source, properties); } - export function fillGlobals(target: Tasks.CommandConfiguration, source: Tasks.CommandConfiguration | undefined, taskName: string | undefined): Tasks.CommandConfiguration { + export function fillGlobals(target: Tasks.ICommandConfiguration, source: Tasks.ICommandConfiguration | undefined, taskName: string | undefined): Tasks.ICommandConfiguration { if ((source === undefined) || isEmpty(source)) { return target; } @@ -1106,7 +1116,7 @@ namespace CommandConfiguration { return target; } - export function fillDefaults(value: Tasks.CommandConfiguration | undefined, context: ParseContext): void { + export function fillDefaults(value: Tasks.ICommandConfiguration | undefined, context: IParseContext): void { if (!value || Object.isFrozen(value)) { return; } @@ -1125,21 +1135,21 @@ namespace CommandConfiguration { } } - export function freeze(value: Tasks.CommandConfiguration): Readonly<Tasks.CommandConfiguration> | undefined { + export function freeze(value: Tasks.ICommandConfiguration): Readonly<Tasks.ICommandConfiguration> | undefined { return _freeze(value, properties); } } -namespace ProblemMatcherConverter { +export namespace ProblemMatcherConverter { - export function namedFrom(this: void, declares: ProblemMatcherConfig.NamedProblemMatcher[] | undefined, context: ParseContext): IStringDictionary<NamedProblemMatcher> { - let result: IStringDictionary<NamedProblemMatcher> = Object.create(null); + export function namedFrom(this: void, declares: ProblemMatcherConfig.INamedProblemMatcher[] | undefined, context: IParseContext): IStringDictionary<INamedProblemMatcher> { + const result: IStringDictionary<INamedProblemMatcher> = Object.create(null); if (!Types.isArray(declares)) { return result; } - (<ProblemMatcherConfig.NamedProblemMatcher[]>declares).forEach((value) => { - let namedProblemMatcher = (new ProblemMatcherParser(context.problemReporter)).parse(value); + (<ProblemMatcherConfig.INamedProblemMatcher[]>declares).forEach((value) => { + const namedProblemMatcher = (new ProblemMatcherParser(context.problemReporter)).parse(value); if (isNamedProblemMatcher(namedProblemMatcher)) { result[namedProblemMatcher.name] = namedProblemMatcher; } else { @@ -1149,7 +1159,7 @@ namespace ProblemMatcherConverter { return result; } - export function fromWithOsConfig(this: void, external: ConfigurationProperties & { [key: string]: any }, context: ParseContext): TaskConfigurationValueWithErrors<ProblemMatcher[]> { + export function fromWithOsConfig(this: void, external: IConfigurationProperties & { [key: string]: any }, context: IParseContext): TaskConfigurationValueWithErrors<ProblemMatcher[]> { let result: TaskConfigurationValueWithErrors<ProblemMatcher[]> = {}; if (external.windows && external.windows.problemMatcher && context.platform === Platform.Windows) { result = from(external.windows.problemMatcher, context); @@ -1163,8 +1173,8 @@ namespace ProblemMatcherConverter { return result; } - export function from(this: void, config: ProblemMatcherConfig.ProblemMatcherType | undefined, context: ParseContext): TaskConfigurationValueWithErrors<ProblemMatcher[]> { - let result: ProblemMatcher[] = []; + export function from(this: void, config: ProblemMatcherConfig.ProblemMatcherType | undefined, context: IParseContext): TaskConfigurationValueWithErrors<ProblemMatcher[]> { + const result: ProblemMatcher[] = []; if (config === undefined) { return { value: result }; } @@ -1177,7 +1187,7 @@ namespace ProblemMatcherConverter { errors.push(...matcher.errors); } } - let kind = getProblemMatcherKind(config); + const kind = getProblemMatcherKind(config); if (kind === ProblemMatcherKind.Unknown) { const error = nls.localize( 'ConfigurationParser.unknownMatcherKind', @@ -1187,7 +1197,7 @@ namespace ProblemMatcherConverter { } else if (kind === ProblemMatcherKind.String || kind === ProblemMatcherKind.ProblemMatcher) { addResult(resolveProblemMatcher(config as ProblemMatcherConfig.ProblemMatcher, context)); } else if (kind === ProblemMatcherKind.Array) { - let problemMatchers = <(string | ProblemMatcherConfig.ProblemMatcher)[]>config; + const problemMatchers = <(string | ProblemMatcherConfig.ProblemMatcher)[]>config; problemMatchers.forEach(problemMatcher => { addResult(resolveProblemMatcher(problemMatcher, context)); }); @@ -1207,16 +1217,16 @@ namespace ProblemMatcherConverter { } } - function resolveProblemMatcher(this: void, value: string | ProblemMatcherConfig.ProblemMatcher, context: ParseContext): TaskConfigurationValueWithErrors<ProblemMatcher> { + function resolveProblemMatcher(this: void, value: string | ProblemMatcherConfig.ProblemMatcher, context: IParseContext): TaskConfigurationValueWithErrors<ProblemMatcher> { if (Types.isString(value)) { let variableName = <string>value; if (variableName.length > 1 && variableName[0] === '$') { variableName = variableName.substring(1); - let global = ProblemMatcherRegistry.get(variableName); + const global = ProblemMatcherRegistry.get(variableName); if (global) { return { value: Objects.deepClone(global) }; } - let localProblemMatcher: ProblemMatcher & Partial<NamedProblemMatcher> = context.namedProblemMatchers[variableName]; + let localProblemMatcher: ProblemMatcher & Partial<INamedProblemMatcher> = context.namedProblemMatchers[variableName]; if (localProblemMatcher) { localProblemMatcher = Objects.deepClone(localProblemMatcher); // remove the name @@ -1226,7 +1236,7 @@ namespace ProblemMatcherConverter { } return { errors: [nls.localize('ConfigurationParser.invalidVariableReference', 'Error: Invalid problemMatcher reference: {0}\n', value)] }; } else { - let json = <ProblemMatcherConfig.ProblemMatcher>value; + const json = <ProblemMatcherConfig.ProblemMatcher>value; return { value: new ProblemMatcherParser(context.problemReporter).parse(json) }; } } @@ -1238,21 +1248,21 @@ const partialSource: Partial<Tasks.TaskSource> = { }; export namespace GroupKind { - export function from(this: void, external: string | GroupKind | undefined): Tasks.TaskGroup | undefined { + export function from(this: void, external: string | IGroupKind | undefined): Tasks.TaskGroup | undefined { if (external === undefined) { return undefined; } else if (Types.isString(external) && Tasks.TaskGroup.is(external)) { return { _id: external, isDefault: false }; } else if (Types.isString(external.kind) && Tasks.TaskGroup.is(external.kind)) { - let group: string = external.kind; - let isDefault: boolean | string = Types.isUndefined(external.isDefault) ? false : external.isDefault; + const group: string = external.kind; + const isDefault: boolean | string = Types.isUndefined(external.isDefault) ? false : external.isDefault; return { _id: group, isDefault }; } return undefined; } - export function to(group: Tasks.TaskGroup | string): GroupKind | string { + export function to(group: Tasks.TaskGroup | string): IGroupKind | string { if (Types.isString(group)) { return group; } else if (!group.isDefault) { @@ -1266,21 +1276,21 @@ export namespace GroupKind { } namespace TaskDependency { - function uriFromSource(context: ParseContext, source: TaskConfigSource): URI | string { + function uriFromSource(context: IParseContext, source: TaskConfigSource): URI | string { switch (source) { - case TaskConfigSource.User: return USER_TASKS_GROUP_KEY; + case TaskConfigSource.User: return Tasks.USER_TASKS_GROUP_KEY; case TaskConfigSource.TasksJson: return context.workspaceFolder.uri; default: return context.workspace && context.workspace.configuration ? context.workspace.configuration : context.workspaceFolder.uri; } } - export function from(this: void, external: string | TaskIdentifier, context: ParseContext, source: TaskConfigSource): Tasks.TaskDependency | undefined { + export function from(this: void, external: string | ITaskIdentifier, context: IParseContext, source: TaskConfigSource): Tasks.ITaskDependency | undefined { if (Types.isString(external)) { return { uri: uriFromSource(context, source), task: external }; - } else if (TaskIdentifier.is(external)) { + } else if (ITaskIdentifier.is(external)) { return { uri: uriFromSource(context, source), - task: Tasks.TaskDefinition.createTaskIdentifier(external as Tasks.TaskIdentifier, context.problemReporter) + task: Tasks.TaskDefinition.createTaskIdentifier(external as Tasks.ITaskIdentifier, context.problemReporter) }; } else { return undefined; @@ -1302,20 +1312,25 @@ namespace DependsOrder { namespace ConfigurationProperties { - const properties: MetaData<Tasks.ConfigurationProperties, any>[] = [ - - { property: 'name' }, { property: 'identifier' }, { property: 'group' }, { property: 'isBackground' }, - { property: 'promptOnClose' }, { property: 'dependsOn' }, - { property: 'presentation', type: CommandConfiguration.PresentationOptions }, { property: 'problemMatchers' }, - { property: 'options' } + const properties: IMetaData<Tasks.IConfigurationProperties, any>[] = [ + { property: 'name' }, + { property: 'identifier' }, + { property: 'group' }, + { property: 'isBackground' }, + { property: 'promptOnClose' }, + { property: 'dependsOn' }, + { property: 'presentation', type: CommandConfiguration.PresentationOptions }, + { property: 'problemMatchers' }, + { property: 'options' }, + { property: 'icon' } ]; - export function from(this: void, external: ConfigurationProperties & { [key: string]: any }, context: ParseContext, - includeCommandOptions: boolean, source: TaskConfigSource, properties?: IJSONSchemaMap): TaskConfigurationValueWithErrors<Tasks.ConfigurationProperties> { + export function from(this: void, external: IConfigurationProperties & { [key: string]: any }, context: IParseContext, + includeCommandOptions: boolean, source: TaskConfigSource, properties?: IJSONSchemaMap): TaskConfigurationValueWithErrors<Tasks.IConfigurationProperties> { if (!external) { return {}; } - let result: Tasks.ConfigurationProperties & { [key: string]: any } = {}; + const result: Tasks.IConfigurationProperties & { [key: string]: any } = {}; if (properties) { for (const propertyName of Object.keys(properties)) { @@ -1334,6 +1349,8 @@ namespace ConfigurationProperties { if (Types.isString(external.identifier)) { result.identifier = external.identifier; } + result.icon = external.icon; + if (external.isBackground !== undefined) { result.isBackground = !!external.isBackground; } @@ -1343,7 +1360,7 @@ namespace ConfigurationProperties { result.group = GroupKind.from(external.group); if (external.dependsOn !== undefined) { if (Types.isArray(external.dependsOn)) { - result.dependsOn = external.dependsOn.reduce((dependencies: Tasks.TaskDependency[], item): Tasks.TaskDependency[] => { + result.dependsOn = external.dependsOn.reduce((dependencies: Tasks.ITaskDependency[], item): Tasks.ITaskDependency[] => { const dependency = TaskDependency.from(item, context, source); if (dependency) { dependencies.push(dependency); @@ -1356,7 +1373,7 @@ namespace ConfigurationProperties { } } result.dependsOrder = DependsOrder.from(external.dependsOrder); - if (includeCommandOptions && (external.presentation !== undefined || (external as LegacyCommandProperties).terminal !== undefined)) { + if (includeCommandOptions && (external.presentation !== undefined || (external as ILegacyCommandProperties).terminal !== undefined)) { result.presentation = CommandConfiguration.PresentationOptions.from(external, context); } if (includeCommandOptions && (external.options !== undefined)) { @@ -1372,7 +1389,7 @@ namespace ConfigurationProperties { return isEmpty(result) ? {} : { value: result, errors: configProblemMatcher.errors }; } - export function isEmpty(this: void, value: Tasks.ConfigurationProperties): boolean { + export function isEmpty(this: void, value: Tasks.IConfigurationProperties): boolean { return _isEmpty(value, properties); } } @@ -1385,27 +1402,27 @@ namespace ConfiguringTask { const npm = 'vscode.npm.'; const typescript = 'vscode.typescript.'; - interface CustomizeShape { + interface ICustomizeShape { customize: string; } - export function from(this: void, external: ConfiguringTask, context: ParseContext, index: number, source: TaskConfigSource): Tasks.ConfiguringTask | undefined { + export function from(this: void, external: IConfiguringTask, context: IParseContext, index: number, source: TaskConfigSource, registry?: Partial<ITaskDefinitionRegistry>): Tasks.ConfiguringTask | undefined { if (!external) { return undefined; } - let type = external.type; - let customize = (external as CustomizeShape).customize; + const type = external.type; + const customize = (external as ICustomizeShape).customize; if (!type && !customize) { context.problemReporter.error(nls.localize('ConfigurationParser.noTaskType', 'Error: tasks configuration must have a type property. The configuration will be ignored.\n{0}\n', JSON.stringify(external, null, 4))); return undefined; } - let typeDeclaration = type ? TaskDefinitionRegistry.get(type) : undefined; + const typeDeclaration = type ? registry?.get?.(type) || TaskDefinitionRegistry.get(type) : undefined; if (!typeDeclaration) { - let message = nls.localize('ConfigurationParser.noTypeDefinition', 'Error: there is no registered task type \'{0}\'. Did you miss installing an extension that provides a corresponding task provider?', type); + const message = nls.localize('ConfigurationParser.noTypeDefinition', 'Error: there is no registered task type \'{0}\'. Did you miss installing an extension that provides a corresponding task provider?', type); context.problemReporter.error(message); return undefined; } - let identifier: Tasks.TaskIdentifier | undefined; + let identifier: Tasks.ITaskIdentifier | undefined; if (Types.isString(customize)) { if (customize.indexOf(grunt) === 0) { identifier = { type: 'grunt', task: customize.substring(grunt.length) }; @@ -1420,7 +1437,7 @@ namespace ConfiguringTask { } } else { if (Types.isString(external.type)) { - identifier = external as Tasks.TaskIdentifier; + identifier = external as Tasks.ITaskIdentifier; } } if (identifier === undefined) { @@ -1430,7 +1447,7 @@ namespace ConfiguringTask { )); return undefined; } - let taskIdentifier: Tasks.KeyedTaskIdentifier | undefined = Tasks.TaskDefinition.createTaskIdentifier(identifier, context.problemReporter); + const taskIdentifier: Tasks.KeyedTaskIdentifier | undefined = Tasks.TaskDefinition.createTaskIdentifier(identifier, context.problemReporter); if (taskIdentifier === undefined) { context.problemReporter.error(nls.localize( 'ConfigurationParser.incorrectType', @@ -1438,7 +1455,7 @@ namespace ConfiguringTask { )); return undefined; } - let configElement: Tasks.TaskSourceConfigElement = { + const configElement: Tasks.ITaskSourceConfigElement = { workspaceFolder: context.workspaceFolder, file: '.vscode/tasks.json', index, @@ -1447,7 +1464,7 @@ namespace ConfiguringTask { let taskSource: Tasks.FileBasedTaskSource; switch (source) { case TaskConfigSource.User: { - taskSource = Object.assign({} as Tasks.UserTaskSource, partialSource, { kind: Tasks.TaskSourceKind.User, config: configElement }); + taskSource = Object.assign({} as Tasks.IUserTaskSource, partialSource, { kind: Tasks.TaskSourceKind.User, config: configElement }); break; } case TaskConfigSource.WorkspaceFile: { @@ -1455,11 +1472,11 @@ namespace ConfiguringTask { break; } default: { - taskSource = Object.assign({} as Tasks.WorkspaceTaskSource, partialSource, { kind: Tasks.TaskSourceKind.Workspace, config: configElement }); + taskSource = Object.assign({} as Tasks.IWorkspaceTaskSource, partialSource, { kind: Tasks.TaskSourceKind.Workspace, config: configElement }); break; } } - let result: Tasks.ConfiguringTask = new Tasks.ConfiguringTask( + const result: Tasks.ConfiguringTask = new Tasks.ConfiguringTask( `${typeDeclaration.extensionId}.${taskIdentifier._key}`, taskSource, undefined, @@ -1468,7 +1485,7 @@ namespace ConfiguringTask { RunOptions.fromConfiguration(external.runOptions), {} ); - let configuration = ConfigurationProperties.from(external, context, true, source, typeDeclaration.properties); + const configuration = ConfigurationProperties.from(external, context, true, source, typeDeclaration.properties); result.addTaskLoadMessages(configuration.errors); if (configuration.value) { result.configurationProperties = Object.assign(result.configurationProperties, configuration.value); @@ -1477,10 +1494,10 @@ namespace ConfiguringTask { } else { let label = result.configures.type; if (typeDeclaration.required && typeDeclaration.required.length > 0) { - for (let required of typeDeclaration.required) { - let value = result.configures[required]; + for (const required of typeDeclaration.required) { + const value = result.configures[required]; if (value) { - label = label + ' ' + value; + label = label + ': ' + value; break; } } @@ -1496,7 +1513,7 @@ namespace ConfiguringTask { } namespace CustomTask { - export function from(this: void, external: CustomTask, context: ParseContext, index: number, source: TaskConfigSource): Tasks.CustomTask | undefined { + export function from(this: void, external: ICustomTask, context: IParseContext, index: number, source: TaskConfigSource): Tasks.CustomTask | undefined { if (!external) { return undefined; } @@ -1520,7 +1537,7 @@ namespace CustomTask { let taskSource: Tasks.FileBasedTaskSource; switch (source) { case TaskConfigSource.User: { - taskSource = Object.assign({} as Tasks.UserTaskSource, partialSource, { kind: Tasks.TaskSourceKind.User, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder } }); + taskSource = Object.assign({} as Tasks.IUserTaskSource, partialSource, { kind: Tasks.TaskSourceKind.User, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder } }); break; } case TaskConfigSource.WorkspaceFile: { @@ -1528,12 +1545,12 @@ namespace CustomTask { break; } default: { - taskSource = Object.assign({} as Tasks.WorkspaceTaskSource, partialSource, { kind: Tasks.TaskSourceKind.Workspace, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder } }); + taskSource = Object.assign({} as Tasks.IWorkspaceTaskSource, partialSource, { kind: Tasks.TaskSourceKind.Workspace, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder } }); break; } } - let result: Tasks.CustomTask = new Tasks.CustomTask( + const result: Tasks.CustomTask = new Tasks.CustomTask( context.uuidMap.getUUID(taskName), taskSource, taskName, @@ -1546,14 +1563,14 @@ namespace CustomTask { identifier: taskName, } ); - let configuration = ConfigurationProperties.from(external, context, false, source); + const configuration = ConfigurationProperties.from(external, context, false, source); result.addTaskLoadMessages(configuration.errors); if (configuration.value) { result.configurationProperties = Object.assign(result.configurationProperties, configuration.value); } - let supportLegacy: boolean = true; //context.schemaVersion === Tasks.JsonSchemaVersion.V2_0_0; + const supportLegacy: boolean = true; //context.schemaVersion === Tasks.JsonSchemaVersion.V2_0_0; if (supportLegacy) { - let legacy: LegacyTaskProperties = external as LegacyTaskProperties; + const legacy: ILegacyTaskProperties = external as ILegacyTaskProperties; if (result.configurationProperties.isBackground === undefined && legacy.isWatching !== undefined) { result.configurationProperties.isBackground = !!legacy.isWatching; } @@ -1565,7 +1582,7 @@ namespace CustomTask { } } } - let command: Tasks.CommandConfiguration = CommandConfiguration.from(external, context)!; + const command: Tasks.ICommandConfiguration = CommandConfiguration.from(external, context)!; if (command) { result.command = command; } @@ -1577,7 +1594,7 @@ namespace CustomTask { return result; } - export function fillGlobals(task: Tasks.CustomTask, globals: Globals): void { + export function fillGlobals(task: Tasks.CustomTask, globals: IGlobals): void { // We only merge a command from a global definition if there is no dependsOn // or there is a dependsOn and a defined command. if (CommandConfiguration.hasCommand(task.command) || task.configurationProperties.dependsOn === undefined) { @@ -1593,7 +1610,7 @@ namespace CustomTask { } } - export function fillDefaults(task: Tasks.CustomTask, context: ParseContext): void { + export function fillDefaults(task: Tasks.CustomTask, context: IParseContext): void { CommandConfiguration.fillDefaults(task.command, context); if (task.configurationProperties.promptOnClose === undefined) { task.configurationProperties.promptOnClose = task.configurationProperties.isBackground !== undefined ? !task.configurationProperties.isBackground : true; @@ -1607,7 +1624,7 @@ namespace CustomTask { } export function createCustomTask(contributedTask: Tasks.ContributedTask, configuredProps: Tasks.ConfiguringTask | Tasks.CustomTask): Tasks.CustomTask { - let result: Tasks.CustomTask = new Tasks.CustomTask( + const result: Tasks.CustomTask = new Tasks.CustomTask( configuredProps._id, Object.assign({}, configuredProps._source, { customizes: contributedTask.defines }), configuredProps.configurationProperties.name || contributedTask._label, @@ -1618,10 +1635,12 @@ namespace CustomTask { { name: configuredProps.configurationProperties.name || contributedTask.configurationProperties.name, identifier: configuredProps.configurationProperties.identifier || contributedTask.configurationProperties.identifier, - } + icon: configuredProps.configurationProperties.icon + }, + ); result.addTaskLoadMessages(configuredProps.taskLoadMessages); - let resultConfigProps: Tasks.ConfigurationProperties = result.configurationProperties; + const resultConfigProps: Tasks.IConfigurationProperties = result.configurationProperties; assignProperty(resultConfigProps, configuredProps.configurationProperties, 'group'); assignProperty(resultConfigProps, configuredProps.configurationProperties, 'isBackground'); @@ -1634,7 +1653,7 @@ namespace CustomTask { result.command.options = CommandOptions.assignProperties(result.command.options, configuredProps.configurationProperties.options); result.runOptions = RunOptions.assignProperties(result.runOptions, configuredProps.runOptions); - let contributedConfigProps: Tasks.ConfigurationProperties = contributedTask.configurationProperties; + const contributedConfigProps: Tasks.IConfigurationProperties = contributedTask.configurationProperties; fillProperty(resultConfigProps, contributedConfigProps, 'group'); fillProperty(resultConfigProps, contributedConfigProps, 'isBackground'); fillProperty(resultConfigProps, contributedConfigProps, 'dependsOn'); @@ -1654,16 +1673,16 @@ namespace CustomTask { } } -interface TaskParseResult { +export interface ITaskParseResult { custom: Tasks.CustomTask[]; configured: Tasks.ConfiguringTask[]; } -namespace TaskParser { +export namespace TaskParser { - function isCustomTask(value: CustomTask | ConfiguringTask): value is CustomTask { - let type = value.type; - let customize = (value as any).customize; + function isCustomTask(value: ICustomTask | IConfiguringTask): value is ICustomTask { + const type = value.type; + const customize = (value as any).customize; return customize === undefined && (type === undefined || type === null || type === Tasks.CUSTOMIZED_TASK_TYPE || type === 'shell' || type === 'process'); } @@ -1672,18 +1691,18 @@ namespace TaskParser { process: ProcessExecutionSupportedContext }; - export function from(this: void, externals: Array<CustomTask | ConfiguringTask> | undefined, globals: Globals, context: ParseContext, source: TaskConfigSource): TaskParseResult { - let result: TaskParseResult = { custom: [], configured: [] }; + export function from(this: void, externals: Array<ICustomTask | IConfiguringTask> | undefined, globals: IGlobals, context: IParseContext, source: TaskConfigSource, registry?: Partial<ITaskDefinitionRegistry>): ITaskParseResult { + const result: ITaskParseResult = { custom: [], configured: [] }; if (!externals) { return result; } - let defaultBuildTask: { task: Tasks.Task | undefined; rank: number } = { task: undefined, rank: -1 }; - let defaultTestTask: { task: Tasks.Task | undefined; rank: number } = { task: undefined, rank: -1 }; - let schema2_0_0: boolean = context.schemaVersion === Tasks.JsonSchemaVersion.V2_0_0; + const defaultBuildTask: { task: Tasks.Task | undefined; rank: number } = { task: undefined, rank: -1 }; + const defaultTestTask: { task: Tasks.Task | undefined; rank: number } = { task: undefined, rank: -1 }; + const schema2_0_0: boolean = context.schemaVersion === Tasks.JsonSchemaVersion.V2_0_0; const baseLoadIssues = Objects.deepClone(context.taskLoadIssues); for (let index = 0; index < externals.length; index++) { - let external = externals[index]; - const definition = external.type ? TaskDefinitionRegistry.get(external.type) : undefined; + const external = externals[index]; + const definition = external.type ? registry?.get?.(external.type) || TaskDefinitionRegistry.get(external.type) : undefined; let typeNotSupported: boolean = false; if (definition && definition.when && !context.contextKeyService.contextMatchesRules(definition.when)) { typeNotSupported = true; @@ -1705,7 +1724,7 @@ namespace TaskParser { } if (isCustomTask(external)) { - let customTask = CustomTask.from(external, context, index, source); + const customTask = CustomTask.from(external, context, index, source); if (customTask) { CustomTask.fillGlobals(customTask, globals); CustomTask.fillDefaults(customTask, context); @@ -1743,7 +1762,7 @@ namespace TaskParser { result.custom.push(customTask); } } else { - let configuredTask = ConfiguringTask.from(external, context, index, source); + const configuredTask = ConfiguringTask.from(external, context, index, source, registry); if (configuredTask) { configuredTask.addTaskLoadMessages(context.taskLoadIssues); result.configured.push(configuredTask); @@ -1775,7 +1794,7 @@ namespace TaskParser { if (source) { // Tasks are keyed by ID but we need to merge by name - let map: IStringDictionary<Tasks.CustomTask> = Object.create(null); + const map: IStringDictionary<Tasks.CustomTask> = Object.create(null); target.forEach((task) => { map[task.configurationProperties.name!] = task; }); @@ -1783,7 +1802,7 @@ namespace TaskParser { source.forEach((task) => { map[task.configurationProperties.name!] = task; }); - let newTarget: Tasks.CustomTask[] = []; + const newTarget: Tasks.CustomTask[] = []; target.forEach(task => { newTarget.push(map[task.configurationProperties.name!]); delete map[task.configurationProperties.name!]; @@ -1795,8 +1814,8 @@ namespace TaskParser { } } -interface Globals { - command?: Tasks.CommandConfiguration; +export interface IGlobals { + command?: Tasks.ICommandConfiguration; problemMatcher?: ProblemMatcher[]; promptOnClose?: boolean; suppressTaskName?: boolean; @@ -1804,9 +1823,9 @@ interface Globals { namespace Globals { - export function from(config: ExternalTaskRunnerConfiguration, context: ParseContext): Globals { + export function from(config: IExternalTaskRunnerConfiguration, context: IParseContext): IGlobals { let result = fromBase(config, context); - let osGlobals: Globals | undefined = undefined; + let osGlobals: IGlobals | undefined = undefined; if (config.windows && context.platform === Platform.Windows) { osGlobals = fromBase(config.windows, context); } else if (config.osx && context.platform === Platform.Mac) { @@ -1817,7 +1836,7 @@ namespace Globals { if (osGlobals) { result = Globals.assignProperties(result, osGlobals); } - let command = CommandConfiguration.from(config, context); + const command = CommandConfiguration.from(config, context); if (command) { result.command = command; } @@ -1826,8 +1845,8 @@ namespace Globals { return result; } - export function fromBase(this: void, config: BaseTaskRunnerConfiguration, context: ParseContext): Globals { - let result: Globals = {}; + export function fromBase(this: void, config: IBaseTaskRunnerConfiguration, context: IParseContext): IGlobals { + const result: IGlobals = {}; if (config.suppressTaskName !== undefined) { result.suppressTaskName = !!config.suppressTaskName; } @@ -1840,11 +1859,11 @@ namespace Globals { return result; } - export function isEmpty(value: Globals): boolean { + export function isEmpty(value: IGlobals): boolean { return !value || value.command === undefined && value.promptOnClose === undefined && value.suppressTaskName === undefined; } - export function assignProperties(target: Globals, source: Globals): Globals { + export function assignProperties(target: IGlobals, source: IGlobals): IGlobals { if (isEmpty(source)) { return target; } @@ -1856,7 +1875,7 @@ namespace Globals { return target; } - export function fillDefaults(value: Globals, context: ParseContext): void { + export function fillDefaults(value: IGlobals, context: IParseContext): void { if (!value) { return; } @@ -1869,7 +1888,7 @@ namespace Globals { } } - export function freeze(value: Globals): void { + export function freeze(value: IGlobals): void { Object.freeze(value); if (value.command) { CommandConfiguration.freeze(value.command); @@ -1879,8 +1898,8 @@ namespace Globals { export namespace ExecutionEngine { - export function from(config: ExternalTaskRunnerConfiguration): Tasks.ExecutionEngine { - let runner = config.runner || config._runner; + export function from(config: IExternalTaskRunnerConfiguration): Tasks.ExecutionEngine { + const runner = config.runner || config._runner; let result: Tasks.ExecutionEngine | undefined; if (runner) { switch (runner) { @@ -1892,7 +1911,7 @@ export namespace ExecutionEngine { break; } } - let schemaVersion = JsonSchemaVersion.from(config); + const schemaVersion = JsonSchemaVersion.from(config); if (schemaVersion === Tasks.JsonSchemaVersion.V0_1_0) { return result || Tasks.ExecutionEngine.Process; } else if (schemaVersion === Tasks.JsonSchemaVersion.V2_0_0) { @@ -1907,8 +1926,8 @@ export namespace JsonSchemaVersion { const _default: Tasks.JsonSchemaVersion = Tasks.JsonSchemaVersion.V2_0_0; - export function from(config: ExternalTaskRunnerConfiguration): Tasks.JsonSchemaVersion { - let version = config.version; + export function from(config: IExternalTaskRunnerConfiguration): Tasks.JsonSchemaVersion { + const version = config.version; if (!version) { return _default; } @@ -1923,7 +1942,7 @@ export namespace JsonSchemaVersion { } } -export interface ParseResult { +export interface IParseResult { validationStatus: ValidationStatus; custom: Tasks.CustomTask[]; configured: Tasks.ConfiguringTask[]; @@ -1933,7 +1952,7 @@ export interface ParseResult { export interface IProblemReporter extends IProblemReporterBase { } -class UUIDMap { +export class UUIDMap { private last: IStringDictionary<string | string[]> | undefined; private current: IStringDictionary<string | string[]>; @@ -1941,8 +1960,8 @@ class UUIDMap { constructor(other?: UUIDMap) { this.current = Object.create(null); if (other) { - for (let key of Object.keys(other.current)) { - let value = other.current[key]; + for (const key of Object.keys(other.current)) { + const value = other.current[key]; if (Array.isArray(value)) { this.current[key] = value.slice(); } else { @@ -1958,7 +1977,7 @@ class UUIDMap { } public getUUID(identifier: string): string { - let lastValue = this.last ? this.last[identifier] : undefined; + const lastValue = this.last ? this.last[identifier] : undefined; let result: string | undefined = undefined; if (lastValue !== undefined) { if (Array.isArray(lastValue)) { @@ -1974,14 +1993,14 @@ class UUIDMap { if (result === undefined) { result = UUID.generateUuid(); } - let currentValue = this.current[identifier]; + const currentValue = this.current[identifier]; if (currentValue === undefined) { this.current[identifier] = result; } else { if (Array.isArray(currentValue)) { currentValue.push(result); } else { - let arrayValue: string[] = [currentValue]; + const arrayValue: string[] = [currentValue]; arrayValue.push(result); this.current[identifier] = arrayValue; } @@ -2016,10 +2035,10 @@ class ConfigurationParser { this.uuidMap = uuidMap; } - public run(fileConfig: ExternalTaskRunnerConfiguration, source: TaskConfigSource, contextKeyService: IContextKeyService): ParseResult { - let engine = ExecutionEngine.from(fileConfig); - let schemaVersion = JsonSchemaVersion.from(fileConfig); - let context: ParseContext = { + public run(fileConfig: IExternalTaskRunnerConfiguration, source: TaskConfigSource, contextKeyService: IContextKeyService): IParseResult { + const engine = ExecutionEngine.from(fileConfig); + const schemaVersion = JsonSchemaVersion.from(fileConfig); + const context: IParseContext = { workspaceFolder: this.workspaceFolder, workspace: this.workspace, problemReporter: this.problemReporter, @@ -2031,7 +2050,7 @@ class ConfigurationParser { taskLoadIssues: [], contextKeyService }; - let taskParseResult = this.createTaskRunnerConfiguration(fileConfig, context, source); + const taskParseResult = this.createTaskRunnerConfiguration(fileConfig, context, source); return { validationStatus: this.problemReporter.status, custom: taskParseResult.custom, @@ -2040,14 +2059,14 @@ class ConfigurationParser { }; } - private createTaskRunnerConfiguration(fileConfig: ExternalTaskRunnerConfiguration, context: ParseContext, source: TaskConfigSource): TaskParseResult { - let globals = Globals.from(fileConfig, context); + private createTaskRunnerConfiguration(fileConfig: IExternalTaskRunnerConfiguration, context: IParseContext, source: TaskConfigSource): ITaskParseResult { + const globals = Globals.from(fileConfig, context); if (this.problemReporter.status.isFatal()) { return { custom: [], configured: [] }; } context.namedProblemMatchers = ProblemMatcherConverter.namedFrom(fileConfig.declares, context); let globalTasks: Tasks.CustomTask[] | undefined = undefined; - let externalGlobalTasks: Array<ConfiguringTask | CustomTask> | undefined = undefined; + let externalGlobalTasks: Array<IConfiguringTask | ICustomTask> | undefined = undefined; if (fileConfig.windows && context.platform === Platform.Windows) { globalTasks = TaskParser.from(fileConfig.windows.tasks, globals, context, source).custom; externalGlobalTasks = fileConfig.windows.tasks; @@ -2059,8 +2078,8 @@ class ConfigurationParser { externalGlobalTasks = fileConfig.linux.tasks; } if (context.schemaVersion === Tasks.JsonSchemaVersion.V2_0_0 && globalTasks && globalTasks.length > 0 && externalGlobalTasks && externalGlobalTasks.length > 0) { - let taskContent: string[] = []; - for (let task of externalGlobalTasks) { + const taskContent: string[] = []; + for (const task of externalGlobalTasks) { taskContent.push(JSON.stringify(task, null, 4)); } context.problemReporter.error( @@ -2070,7 +2089,7 @@ class ConfigurationParser { ); } - let result: TaskParseResult = { custom: [], configured: [] }; + let result: ITaskParseResult = { custom: [], configured: [] }; if (fileConfig.tasks) { result = TaskParser.from(fileConfig.tasks, globals, context, source); } @@ -2080,11 +2099,11 @@ class ConfigurationParser { if ((!result.custom || result.custom.length === 0) && (globals.command && globals.command.name)) { const matchers: ProblemMatcher[] = ProblemMatcherConverter.from(fileConfig.problemMatcher, context).value ?? []; - let isBackground = fileConfig.isBackground ? !!fileConfig.isBackground : fileConfig.isWatching ? !!fileConfig.isWatching : undefined; - let name = Tasks.CommandString.value(globals.command.name); - let task: Tasks.CustomTask = new Tasks.CustomTask( + const isBackground = fileConfig.isBackground ? !!fileConfig.isBackground : fileConfig.isWatching ? !!fileConfig.isWatching : undefined; + const name = Tasks.CommandString.value(globals.command.name); + const task: Tasks.CustomTask = new Tasks.CustomTask( context.uuidMap.getUUID(name), - Object.assign({} as Tasks.WorkspaceTaskSource, source, { config: { index: -1, element: fileConfig, workspaceFolder: context.workspaceFolder } }), + Object.assign({} as Tasks.IWorkspaceTaskSource, source, { config: { index: -1, element: fileConfig, workspaceFolder: context.workspaceFolder } }), name, Tasks.CUSTOMIZED_TASK_TYPE, { @@ -2103,7 +2122,7 @@ class ConfigurationParser { problemMatchers: matchers, } ); - let taskGroupKind = GroupKind.from(fileConfig.group); + const taskGroupKind = GroupKind.from(fileConfig.group); if (taskGroupKind !== undefined) { task.configurationProperties.group = taskGroupKind; } else if (fileConfig.group === 'none') { @@ -2119,10 +2138,10 @@ class ConfigurationParser { } } -let uuidMaps: Map<TaskConfigSource, Map<string, UUIDMap>> = new Map(); -let recentUuidMaps: Map<TaskConfigSource, Map<string, UUIDMap>> = new Map(); -export function parse(workspaceFolder: IWorkspaceFolder, workspace: IWorkspace | undefined, platform: Platform, configuration: ExternalTaskRunnerConfiguration, logger: IProblemReporter, source: TaskConfigSource, contextKeyService: IContextKeyService, isRecents: boolean = false): ParseResult { - let recentOrOtherMaps = isRecents ? recentUuidMaps : uuidMaps; +const uuidMaps: Map<TaskConfigSource, Map<string, UUIDMap>> = new Map(); +const recentUuidMaps: Map<TaskConfigSource, Map<string, UUIDMap>> = new Map(); +export function parse(workspaceFolder: IWorkspaceFolder, workspace: IWorkspace | undefined, platform: Platform, configuration: IExternalTaskRunnerConfiguration, logger: IProblemReporter, source: TaskConfigSource, contextKeyService: IContextKeyService, isRecents: boolean = false): IParseResult { + const recentOrOtherMaps = isRecents ? recentUuidMaps : uuidMaps; let selectedUuidMaps = recentOrOtherMaps.get(source); if (!selectedUuidMaps) { recentOrOtherMaps.set(source, new Map()); @@ -2146,4 +2165,3 @@ export function parse(workspaceFolder: IWorkspaceFolder, workspace: IWorkspace | export function createCustomTask(contributedTask: Tasks.ContributedTask, configuredProps: Tasks.ConfiguringTask | Tasks.CustomTask): Tasks.CustomTask { return CustomTask.createCustomTask(contributedTask, configuredProps); } - diff --git a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts index 8d612f6d9a6..458c4af1d7e 100644 --- a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts +++ b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts @@ -47,25 +47,25 @@ const taskDefinitionSchema: IJSONSchema = { }; namespace Configuration { - export interface TaskDefinition { + export interface ITaskDefinition { type?: string; required?: string[]; properties?: IJSONSchemaMap; when?: string; } - export function from(value: TaskDefinition, extensionId: ExtensionIdentifier, messageCollector: ExtensionMessageCollector): Tasks.TaskDefinition | undefined { + export function from(value: ITaskDefinition, extensionId: ExtensionIdentifier, messageCollector: ExtensionMessageCollector): Tasks.ITaskDefinition | undefined { if (!value) { return undefined; } - let taskType = Types.isString(value.type) ? value.type : undefined; + const taskType = Types.isString(value.type) ? value.type : undefined; if (!taskType || taskType.length === 0) { messageCollector.error(nls.localize('TaskTypeConfiguration.noType', 'The task type configuration is missing the required \'taskType\' property')); return undefined; } - let required: string[] = []; + const required: string[] = []; if (Array.isArray(value.required)) { - for (let element of value.required) { + for (const element of value.required) { if (Types.isString(element)) { required.push(element); } @@ -81,7 +81,7 @@ namespace Configuration { } -const taskDefinitionsExtPoint = ExtensionsRegistry.registerExtensionPoint<Configuration.TaskDefinition[]>({ +const taskDefinitionsExtPoint = ExtensionsRegistry.registerExtensionPoint<Configuration.ITaskDefinition[]>({ extensionPoint: 'taskDefinitions', jsonSchema: { description: nls.localize('TaskDefinitionExtPoint', 'Contributes task kinds'), @@ -93,15 +93,15 @@ const taskDefinitionsExtPoint = ExtensionsRegistry.registerExtensionPoint<Config export interface ITaskDefinitionRegistry { onReady(): Promise<void>; - get(key: string): Tasks.TaskDefinition; - all(): Tasks.TaskDefinition[]; + get(key: string): Tasks.ITaskDefinition; + all(): Tasks.ITaskDefinition[]; getJsonSchema(): IJSONSchema; onDefinitionsChanged: Event<void>; } -class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { +export class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { - private taskTypes: IStringDictionary<Tasks.TaskDefinition>; + private taskTypes: IStringDictionary<Tasks.ITaskDefinition>; private readyPromise: Promise<void>; private _schema: IJSONSchema | undefined; private _onDefinitionsChanged: Emitter<void> = new Emitter(); @@ -111,19 +111,20 @@ class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { this.taskTypes = Object.create(null); this.readyPromise = new Promise<void>((resolve, reject) => { taskDefinitionsExtPoint.setHandler((extensions, delta) => { + this._schema = undefined; try { - for (let extension of delta.removed) { - let taskTypes = extension.value; - for (let taskType of taskTypes) { + for (const extension of delta.removed) { + const taskTypes = extension.value; + for (const taskType of taskTypes) { if (this.taskTypes && taskType.type && this.taskTypes[taskType.type]) { delete this.taskTypes[taskType.type]; } } } - for (let extension of delta.added) { - let taskTypes = extension.value; - for (let taskType of taskTypes) { - let type = Configuration.from(taskType, extension.description.identifier, extension.collector); + for (const extension of delta.added) { + const taskTypes = extension.value; + for (const taskType of taskTypes) { + const type = Configuration.from(taskType, extension.description.identifier, extension.collector); if (type) { this.taskTypes[type.taskType] = type; } @@ -143,19 +144,19 @@ class TaskDefinitionRegistryImpl implements ITaskDefinitionRegistry { return this.readyPromise; } - public get(key: string): Tasks.TaskDefinition { + public get(key: string): Tasks.ITaskDefinition { return this.taskTypes[key]; } - public all(): Tasks.TaskDefinition[] { + public all(): Tasks.ITaskDefinition[] { return Object.keys(this.taskTypes).map(key => this.taskTypes[key]); } public getJsonSchema(): IJSONSchema { if (this._schema === undefined) { - let schemas: IJSONSchema[] = []; - for (let definition of this.all()) { - let schema: IJSONSchema = { + const schemas: IJSONSchema[] = []; + for (const definition of this.all()) { + const schema: IJSONSchema = { type: 'object', additionalProperties: false }; diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index 68a2394a880..86acc2a6a03 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -10,12 +10,12 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IDisposable } from 'vs/base/common/lifecycle'; import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; -import { Task, ContributedTask, CustomTask, TaskSet, TaskSorter, TaskEvent, TaskIdentifier, ConfiguringTask, TaskRunSource } from 'vs/workbench/contrib/tasks/common/tasks'; -import { ITaskSummary, TaskTerminateResponse, TaskSystemInfo } from 'vs/workbench/contrib/tasks/common/taskSystem'; +import { Task, ContributedTask, CustomTask, ITaskSet, TaskSorter, ITaskEvent, ITaskIdentifier, ConfiguringTask, TaskRunSource } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskSummary, ITaskTerminateResponse, ITaskSystemInfo } from 'vs/workbench/contrib/tasks/common/taskSystem'; import { IStringDictionary } from 'vs/base/common/collections'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -export { ITaskSummary, Task, TaskTerminateResponse }; +export { ITaskSummary, Task, ITaskTerminateResponse as TaskTerminateResponse }; export const CustomExecutionSupportedContext = new RawContextKey<boolean>('customExecutionSupported', true, nls.localize('tasks.customExecutionSupported', "Whether CustomExecution tasks are supported. Consider using in the when clause of a \'taskDefinition\' contribution.")); export const ShellExecutionSupportedContext = new RawContextKey<boolean>('shellExecutionSupported', false, nls.localize('tasks.shellExecutionSupported', "Whether ShellExecution tasks are supported. Consider using in the when clause of a \'taskDefinition\' contribution.")); @@ -24,59 +24,59 @@ export const ProcessExecutionSupportedContext = new RawContextKey<boolean>('proc export const ITaskService = createDecorator<ITaskService>('taskService'); export interface ITaskProvider { - provideTasks(validTypes: IStringDictionary<boolean>): Promise<TaskSet>; + provideTasks(validTypes: IStringDictionary<boolean>): Promise<ITaskSet>; resolveTask(task: ConfiguringTask): Promise<ContributedTask | undefined>; } -export interface ProblemMatcherRunOptions { +export interface IProblemMatcherRunOptions { attachProblemMatcher?: boolean; } -export interface CustomizationProperties { +export interface ICustomizationProperties { group?: string | { kind?: string; isDefault?: boolean }; problemMatcher?: string | string[]; isBackground?: boolean; + color?: string; + icon?: string; } -export interface TaskFilter { +export interface ITaskFilter { version?: string; type?: string; } -interface WorkspaceTaskResult { - set: TaskSet | undefined; +interface IWorkspaceTaskResult { + set: ITaskSet | undefined; configurations: { byIdentifier: IStringDictionary<ConfiguringTask>; } | undefined; hasErrors: boolean; } -export interface WorkspaceFolderTaskResult extends WorkspaceTaskResult { +export interface IWorkspaceFolderTaskResult extends IWorkspaceTaskResult { workspaceFolder: IWorkspaceFolder; } -export const USER_TASKS_GROUP_KEY = 'settings'; - export interface ITaskService { readonly _serviceBrand: undefined; - onDidStateChange: Event<TaskEvent>; + onDidStateChange: Event<ITaskEvent>; supportsMultipleTaskExecutions: boolean; configureAction(): Action; - run(task: Task | undefined, options?: ProblemMatcherRunOptions): Promise<ITaskSummary | undefined>; + run(task: Task | undefined, options?: IProblemMatcherRunOptions): Promise<ITaskSummary | undefined>; inTerminal(): boolean; getActiveTasks(): Promise<Task[]>; getBusyTasks(): Promise<Task[]>; - terminate(task: Task): Promise<TaskTerminateResponse>; - tasks(filter?: TaskFilter): Promise<Task[]>; + terminate(task: Task): Promise<ITaskTerminateResponse>; + tasks(filter?: ITaskFilter): Promise<Task[]>; taskTypes(): string[]; - getWorkspaceTasks(runSource?: TaskRunSource): Promise<Map<string, WorkspaceFolderTaskResult>>; + getWorkspaceTasks(runSource?: TaskRunSource): Promise<Map<string, IWorkspaceFolderTaskResult>>; readRecentTasks(): Promise<(Task | ConfiguringTask)[]>; removeRecentlyUsedTask(taskRecentlyUsedKey: string): void; /** * @param alias The task's name, label or defined identifier. */ - getTask(workspaceFolder: IWorkspace | IWorkspaceFolder | string, alias: string | TaskIdentifier, compareId?: boolean): Promise<Task | undefined>; + getTask(workspaceFolder: IWorkspace | IWorkspaceFolder | string, alias: string | ITaskIdentifier, compareId?: boolean): Promise<Task | undefined>; tryResolveTask(configuringTask: ConfiguringTask): Promise<Task | undefined>; createSorter(): TaskSorter; @@ -86,7 +86,7 @@ export interface ITaskService { registerTaskProvider(taskProvider: ITaskProvider, type: string): IDisposable; - registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void; + registerTaskSystem(scheme: string, taskSystemInfo: ITaskSystemInfo): void; onDidChangeTaskSystemInfo: Event<void>; readonly hasTaskSystemInfo: boolean; registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): void; diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index aa7e464bb63..cd204106e8e 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -9,7 +9,7 @@ import { TerminateResponse } from 'vs/base/common/processes'; import { Event } from 'vs/base/common/event'; import { Platform } from 'vs/base/common/platform'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { Task, TaskEvent, KeyedTaskIdentifier } from './tasks'; +import { Task, ITaskEvent, KeyedTaskIdentifier } from './tasks'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; export const enum TaskErrors { @@ -35,37 +35,9 @@ export class TaskError { } } -/* __GDPR__FRAGMENT__ - "TelemetryEvent" : { - "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "runner": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "taskKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "command": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "success": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "exitCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } -*/ -export interface TelemetryEvent { - // How the task got trigger. Is either shortcut or command - trigger: string; - - runner: 'terminal' | 'output'; - - taskKind: string; - - // The command triggered - command: string; - - // Whether the task ran successful - success: boolean; - - // The exit code - exitCode?: number; -} - export namespace Triggers { - export let shortcut: string = 'shortcut'; - export let command: string = 'command'; + export const shortcut: string = 'shortcut'; + export const command: string = 'command'; } export interface ITaskSummary { @@ -97,11 +69,11 @@ export interface ITaskResolver { resolve(uri: URI | string, identifier: string | KeyedTaskIdentifier | undefined): Promise<Task | undefined>; } -export interface TaskTerminateResponse extends TerminateResponse { +export interface ITaskTerminateResponse extends TerminateResponse { task: Task | undefined; } -export interface ResolveSet { +export interface IResolveSet { process?: { name: string; cwd?: string; @@ -110,25 +82,25 @@ export interface ResolveSet { variables: Set<string>; } -export interface ResolvedVariables { +export interface IResolvedVariables { process?: string; variables: Map<string, string>; } -export interface TaskSystemInfo { +export interface ITaskSystemInfo { platform: Platform; context: any; uriProvider: (this: void, path: string) => URI; - resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise<ResolvedVariables | undefined>; + resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: IResolveSet, target: ConfigurationTarget): Promise<IResolvedVariables | undefined>; findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string | undefined>; } -export interface TaskSystemInfoResolver { - (workspaceFolder: IWorkspaceFolder | undefined): TaskSystemInfo | undefined; +export interface ITaskSystemInfoResolver { + (workspaceFolder: IWorkspaceFolder | undefined): ITaskSystemInfo | undefined; } export interface ITaskSystem { - onDidStateChange: Event<TaskEvent>; + onDidStateChange: Event<ITaskEvent>; run(task: Task, resolver: ITaskResolver): ITaskExecuteResult; rerun(): ITaskExecuteResult | undefined; isActive(): Promise<boolean>; @@ -137,8 +109,8 @@ export interface ITaskSystem { getLastInstance(task: Task): Task | undefined; getBusyTasks(): Task[]; canAutoTerminate(): boolean; - terminate(task: Task): Promise<TaskTerminateResponse>; - terminateAll(): Promise<TaskTerminateResponse[]>; + terminate(task: Task): Promise<ITaskTerminateResponse>; + terminateAll(): Promise<ITaskTerminateResponse[]>; revealTask(task: Task): boolean; customExecutionComplete(task: Task, result: number): Promise<void>; isTaskVisible(task: Task): boolean; diff --git a/src/vs/workbench/contrib/tasks/common/taskTemplates.ts b/src/vs/workbench/contrib/tasks/common/taskTemplates.ts index c367ede9ab7..a53c07420d7 100644 --- a/src/vs/workbench/contrib/tasks/common/taskTemplates.ts +++ b/src/vs/workbench/contrib/tasks/common/taskTemplates.ts @@ -7,13 +7,13 @@ import * as nls from 'vs/nls'; import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -export interface TaskEntry extends IQuickPickItem { +export interface ITaskEntry extends IQuickPickItem { sort?: string; autoDetect: boolean; content: string; } -const dotnetBuild: TaskEntry = { +const dotnetBuild: ITaskEntry = { id: 'dotnetCore', label: '.NET Core', sort: 'NET Core', @@ -47,7 +47,7 @@ const dotnetBuild: TaskEntry = { ].join('\n') }; -const msbuild: TaskEntry = { +const msbuild: ITaskEntry = { id: 'msbuild', label: 'MSBuild', autoDetect: false, @@ -82,7 +82,7 @@ const msbuild: TaskEntry = { ].join('\n') }; -const command: TaskEntry = { +const command: ITaskEntry = { id: 'externalCommand', label: 'Others', autoDetect: false, @@ -103,7 +103,7 @@ const command: TaskEntry = { ].join('\n') }; -const maven: TaskEntry = { +const maven: ITaskEntry = { id: 'maven', label: 'maven', sort: 'MVN', @@ -132,8 +132,8 @@ const maven: TaskEntry = { ].join('\n') }; -let _templates: TaskEntry[] | null = null; -export function getTemplates(): TaskEntry[] { +let _templates: ITaskEntry[] | null = null; +export function getTemplates(): ITaskEntry[] { if (!_templates) { _templates = [dotnetBuild, msbuild, maven].sort((a, b) => { return (a.sort || a.label).localeCompare(b.sort || b.label); diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 1e7e42b6c81..1b52c33d02a 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -16,7 +16,9 @@ import { RawContextKey, ContextKeyExpression } from 'vs/platform/contextkey/comm import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/taskService'; + + +export const USER_TASKS_GROUP_KEY = 'settings'; export const TASK_RUNNING_STATE = new RawContextKey<boolean>('taskRunning', false, nls.localize('tasks.taskRunningContext', "Whether a task is currently running.")); export const TASKS_CATEGORY = { value: nls.localize('tasksCategory', "Tasks"), original: 'Tasks' }; @@ -58,7 +60,7 @@ export namespace ShellQuoting { } } -export interface ShellQuotingOptions { +export interface IShellQuotingOptions { /** * The character used to do character escaping. */ @@ -78,7 +80,7 @@ export interface ShellQuotingOptions { weak?: string; } -export interface ShellConfiguration { +export interface IShellConfiguration { /** * The shell executable. */ @@ -92,7 +94,7 @@ export interface ShellConfiguration { /** * Which kind of quotes the shell supports. */ - quoting?: ShellQuotingOptions; + quoting?: IShellQuotingOptions; } export interface CommandOptions { @@ -100,7 +102,7 @@ export interface CommandOptions { /** * The shell to use if the task is a shell command. */ - shell?: ShellConfiguration; + shell?: IShellConfiguration; /** * The current working directory of the executed program or shell. @@ -221,7 +223,7 @@ export namespace PanelKind { } } -export interface PresentationOptions { +export interface IPresentationOptions { /** * Controls whether the task output is reveal in the user interface. * Defaults to `RevealKind.Always`. @@ -274,7 +276,7 @@ export interface PresentationOptions { } export namespace PresentationOptions { - export const defaults: PresentationOptions = { + export const defaults: IPresentationOptions = { echo: true, reveal: RevealKind.Always, revealProblems: RevealProblemKind.Never, focus: false, panel: PanelKind.Shared, showReuseMessage: true, clear: false }; } @@ -308,12 +310,12 @@ export namespace RuntimeType { } } -export interface QuotedString { +export interface IQuotedString { value: string; quoting: ShellQuoting; } -export type CommandString = string | QuotedString; +export type CommandString = string | IQuotedString; export namespace CommandString { export function value(value: CommandString): string { @@ -325,7 +327,7 @@ export namespace CommandString { } } -export interface CommandConfiguration { +export interface ICommandConfiguration { /** * The task type @@ -361,7 +363,7 @@ export interface CommandConfiguration { /** * Describes how the task is presented in the UI. */ - presentation?: PresentationOptions; + presentation?: IPresentationOptions; } export namespace TaskGroup { @@ -418,7 +420,7 @@ export namespace TaskSourceKind { } } -export interface TaskSourceConfigElement { +export interface ITaskSourceConfigElement { workspaceFolder?: IWorkspaceFolder; workspace?: IWorkspace; file: string; @@ -426,57 +428,57 @@ export interface TaskSourceConfigElement { element: any; } -interface BaseTaskSource { +interface IBaseTaskSource { readonly kind: string; readonly label: string; } -export interface WorkspaceTaskSource extends BaseTaskSource { +export interface IWorkspaceTaskSource extends IBaseTaskSource { readonly kind: 'workspace'; - readonly config: TaskSourceConfigElement; + readonly config: ITaskSourceConfigElement; readonly customizes?: KeyedTaskIdentifier; } -export interface ExtensionTaskSource extends BaseTaskSource { +export interface IExtensionTaskSource extends IBaseTaskSource { readonly kind: 'extension'; readonly extension?: string; readonly scope: TaskScope; readonly workspaceFolder: IWorkspaceFolder | undefined; } -export interface ExtensionTaskSourceTransfer { +export interface IExtensionTaskSourceTransfer { __workspaceFolder: UriComponents; __definition: { type: string;[name: string]: any }; } -export interface InMemoryTaskSource extends BaseTaskSource { +export interface IInMemoryTaskSource extends IBaseTaskSource { readonly kind: 'inMemory'; } -export interface UserTaskSource extends BaseTaskSource { +export interface IUserTaskSource extends IBaseTaskSource { readonly kind: 'user'; - readonly config: TaskSourceConfigElement; + readonly config: ITaskSourceConfigElement; readonly customizes?: KeyedTaskIdentifier; } -export interface WorkspaceFileTaskSource extends BaseTaskSource { +export interface WorkspaceFileTaskSource extends IBaseTaskSource { readonly kind: 'workspaceFile'; - readonly config: TaskSourceConfigElement; + readonly config: ITaskSourceConfigElement; readonly customizes?: KeyedTaskIdentifier; } -export type TaskSource = WorkspaceTaskSource | ExtensionTaskSource | InMemoryTaskSource | UserTaskSource | WorkspaceFileTaskSource; -export type FileBasedTaskSource = WorkspaceTaskSource | UserTaskSource | WorkspaceFileTaskSource; -export interface TaskIdentifier { +export type TaskSource = IWorkspaceTaskSource | IExtensionTaskSource | IInMemoryTaskSource | IUserTaskSource | WorkspaceFileTaskSource; +export type FileBasedTaskSource = IWorkspaceTaskSource | IUserTaskSource | WorkspaceFileTaskSource; +export interface ITaskIdentifier { type: string; [name: string]: any; } -export interface KeyedTaskIdentifier extends TaskIdentifier { +export interface KeyedTaskIdentifier extends ITaskIdentifier { _key: string; } -export interface TaskDependency { +export interface ITaskDependency { uri: URI | string; task: string | KeyedTaskIdentifier | undefined; } @@ -486,7 +488,7 @@ export const enum DependsOrder { sequence = 'sequence' } -export interface ConfigurationProperties { +export interface IConfigurationProperties { /** * The task's name @@ -506,7 +508,7 @@ export interface ConfigurationProperties { /** * The presentation options */ - presentation?: PresentationOptions; + presentation?: IPresentationOptions; /** * The command options; @@ -526,7 +528,7 @@ export interface ConfigurationProperties { /** * The other tasks this task depends on. */ - dependsOn?: TaskDependency[]; + dependsOn?: ITaskDependency[]; /** * The order the dependsOn tasks should be executed in. @@ -542,6 +544,11 @@ export interface ConfigurationProperties { * The problem watchers to use for this task */ problemMatchers?: Array<string | ProblemMatcher>; + + /** + * The icon for this task in the terminal tabs list + */ + icon?: { id?: string; color?: string }; } export enum RunOnOptions { @@ -549,14 +556,14 @@ export enum RunOnOptions { folderOpen = 2 } -export interface RunOptions { +export interface IRunOptions { reevaluateOnRerun?: boolean; runOn?: RunOnOptions; instanceLimit?: number; } export namespace RunOptions { - export const defaults: RunOptions = { reevaluateOnRerun: true, runOn: RunOnOptions.default, instanceLimit: 1 }; + export const defaults: IRunOptions = { reevaluateOnRerun: true, runOn: RunOnOptions.default, instanceLimit: 1 }; } export abstract class CommonTask { @@ -573,16 +580,16 @@ export abstract class CommonTask { type?: string; - runOptions: RunOptions; + runOptions: IRunOptions; - configurationProperties: ConfigurationProperties; + configurationProperties: IConfigurationProperties; - _source: BaseTaskSource; + _source: IBaseTaskSource; private _taskLoadMessages: string[] | undefined; - protected constructor(id: string, label: string | undefined, type: string | undefined, runOptions: RunOptions, - configurationProperties: ConfigurationProperties, source: BaseTaskSource) { + protected constructor(id: string, label: string | undefined, type: string | undefined, runOptions: IRunOptions, + configurationProperties: IConfigurationProperties, source: IBaseTaskSource) { this._id = id; if (label) { this._label = label; @@ -610,12 +617,12 @@ export abstract class CommonTask { protected abstract getFolderId(): string | undefined; public getCommonTaskId(): string { - interface RecentTaskKey { + interface IRecentTaskKey { folder: string | undefined; id: string; } - const key: RecentTaskKey = { folder: this.getFolderId(), id: this._id }; + const key: IRecentTaskKey = { folder: this.getFolderId(), id: this._id }; return JSON.stringify(key); } @@ -644,12 +651,12 @@ export abstract class CommonTask { if (Types.isString(key)) { return key === this._label || key === this.configurationProperties.identifier || (compareId && key === this._id); } - let identifier = this.getDefinition(true); + const identifier = this.getDefinition(true); return identifier !== undefined && identifier._key === key._key; } public getQualifiedLabel(): string { - let workspaceFolder = this.getWorkspaceFolder(); + const workspaceFolder = this.getWorkspaceFolder(); if (workspaceFolder) { return `${this._label} (${workspaceFolder.name})`; } else { @@ -657,8 +664,8 @@ export abstract class CommonTask { } } - public getTaskExecution(): TaskExecution { - let result: TaskExecution = { + public getTaskExecution(): ITaskExecution { + const result: ITaskExecution = { id: this._id, task: <any>this }; @@ -679,6 +686,12 @@ export abstract class CommonTask { } } +/** + * For tasks of type shell or process, this is created upon parse + * of the tasks.json or workspace file. + * For ContributedTasks of all other types, this is the result of + * resolving a ConfiguringTask. + */ export class CustomTask extends CommonTask { override type!: '$customized'; // CUSTOMIZED_TASK_TYPE @@ -695,10 +708,10 @@ export class CustomTask extends CommonTask { /** * The command configuration */ - command: CommandConfiguration = {}; + command: ICommandConfiguration = {}; - public constructor(id: string, source: FileBasedTaskSource, label: string, type: string, command: CommandConfiguration | undefined, - hasDefinedMatchers: boolean, runOptions: RunOptions, configurationProperties: ConfigurationProperties) { + public constructor(id: string, source: FileBasedTaskSource, label: string, type: string, command: ICommandConfiguration | undefined, + hasDefinedMatchers: boolean, runOptions: IRunOptions, configurationProperties: IConfigurationProperties) { super(id, label, undefined, runOptions, configurationProperties, source); this._source = source; this.hasDefinedMatchers = hasDefinedMatchers; @@ -745,7 +758,7 @@ export class CustomTask extends CommonTask { throw new Error('Unexpected task runtime'); } - let result: KeyedTaskIdentifier = { + const result: KeyedTaskIdentifier = { type, _key: this._id, id: this._id @@ -759,7 +772,7 @@ export class CustomTask extends CommonTask { } public override getMapKey(): string { - let workspaceFolder = this._source.config.workspaceFolder; + const workspaceFolder = this._source.config.workspaceFolder; return workspaceFolder ? `${workspaceFolder.uri.toString()}|${this._id}|${this.instance}` : `${this._id}|${this.instance}`; } @@ -772,12 +785,12 @@ export class CustomTask extends CommonTask { } public override getRecentlyUsedKey(): string | undefined { - interface CustomKey { + interface ICustomKey { type: string; folder: string; id: string; } - let workspaceFolder = this.getFolderId(); + const workspaceFolder = this.getFolderId(); if (!workspaceFolder) { return undefined; } @@ -785,7 +798,7 @@ export class CustomTask extends CommonTask { if (this._source.kind !== TaskSourceKind.Workspace) { id += this._source.kind; } - let key: CustomKey = { type: CUSTOMIZED_TASK_TYPE, folder: workspaceFolder, id }; + const key: ICustomKey = { type: CUSTOMIZED_TASK_TYPE, folder: workspaceFolder, id }; return JSON.stringify(key); } @@ -810,6 +823,11 @@ export class CustomTask extends CommonTask { } } +/** + * After a contributed task has been parsed, but before + * the task has been resolved via the extension, its properties + * are stored in this + */ export class ConfiguringTask extends CommonTask { /** @@ -820,7 +838,7 @@ export class ConfiguringTask extends CommonTask { configures: KeyedTaskIdentifier; public constructor(id: string, source: FileBasedTaskSource, label: string | undefined, type: string | undefined, - configures: KeyedTaskIdentifier, runOptions: RunOptions, configurationProperties: ConfigurationProperties) { + configures: KeyedTaskIdentifier, runOptions: IRunOptions, configurationProperties: IConfigurationProperties) { super(id, label, type, runOptions, configurationProperties, source); this._source = source; this.configures = configures; @@ -851,12 +869,12 @@ export class ConfiguringTask extends CommonTask { } public override getRecentlyUsedKey(): string | undefined { - interface CustomKey { + interface ICustomKey { type: string; folder: string; id: string; } - let workspaceFolder = this.getFolderId(); + const workspaceFolder = this.getFolderId(); if (!workspaceFolder) { return undefined; } @@ -864,18 +882,21 @@ export class ConfiguringTask extends CommonTask { if (this._source.kind !== TaskSourceKind.Workspace) { id += this._source.kind; } - let key: CustomKey = { type: CUSTOMIZED_TASK_TYPE, folder: workspaceFolder, id }; + const key: ICustomKey = { type: CUSTOMIZED_TASK_TYPE, folder: workspaceFolder, id }; return JSON.stringify(key); } } +/** + * A task from an extension created via resolveTask or provideTask + */ export class ContributedTask extends CommonTask { /** * Indicated the source of the task (e.g. tasks.json or extension) * Set in the super constructor */ - override _source!: ExtensionTaskSource; + override _source!: IExtensionTaskSource; instance: number | undefined; @@ -886,15 +907,21 @@ export class ContributedTask extends CommonTask { /** * The command configuration */ - command: CommandConfiguration; + command: ICommandConfiguration; + + /** + * The icon for the task + */ + icon: { id?: string; color?: string } | undefined; - public constructor(id: string, source: ExtensionTaskSource, label: string, type: string | undefined, defines: KeyedTaskIdentifier, - command: CommandConfiguration, hasDefinedMatchers: boolean, runOptions: RunOptions, - configurationProperties: ConfigurationProperties) { + public constructor(id: string, source: IExtensionTaskSource, label: string, type: string | undefined, defines: KeyedTaskIdentifier, + command: ICommandConfiguration, hasDefinedMatchers: boolean, runOptions: IRunOptions, + configurationProperties: IConfigurationProperties) { super(id, label, type, runOptions, configurationProperties, source); this.defines = defines; this.hasDefinedMatchers = hasDefinedMatchers; this.command = command; + this.icon = configurationProperties.icon; } public override clone(): ContributedTask { @@ -910,7 +937,7 @@ export class ContributedTask extends CommonTask { } public override getMapKey(): string { - let workspaceFolder = this._source.workspaceFolder; + const workspaceFolder = this._source.workspaceFolder; return workspaceFolder ? `${this._source.scope.toString()}|${workspaceFolder.uri.toString()}|${this._id}|${this.instance}` : `${this._source.scope.toString()}|${this._id}|${this.instance}`; @@ -924,14 +951,14 @@ export class ContributedTask extends CommonTask { } public override getRecentlyUsedKey(): string | undefined { - interface ContributedKey { + interface IContributedKey { type: string; scope: number; folder?: string; id: string; } - let key: ContributedKey = { type: 'contributed', scope: this._source.scope, id: this._id }; + const key: IContributedKey = { type: 'contributed', scope: this._source.scope, id: this._id }; key.folder = this.getFolderId(); return JSON.stringify(key); } @@ -953,14 +980,14 @@ export class InMemoryTask extends CommonTask { /** * Indicated the source of the task (e.g. tasks.json or extension) */ - override _source: InMemoryTaskSource; + override _source: IInMemoryTaskSource; instance: number | undefined; override type!: 'inMemory'; - public constructor(id: string, source: InMemoryTaskSource, label: string, type: string, - runOptions: RunOptions, configurationProperties: ConfigurationProperties) { + public constructor(id: string, source: IInMemoryTaskSource, label: string, type: string, + runOptions: IRunOptions, configurationProperties: IConfigurationProperties) { super(id, label, type, runOptions, configurationProperties, source); this._source = source; } @@ -992,7 +1019,7 @@ export class InMemoryTask extends CommonTask { export type Task = CustomTask | ContributedTask | InMemoryTask; -export interface TaskExecution { +export interface ITaskExecution { id: string; task: Task; } @@ -1011,12 +1038,12 @@ export const enum JsonSchemaVersion { V2_0_0 = 2 } -export interface TaskSet { +export interface ITaskSet { tasks: Task[]; extension?: IExtensionDescription; } -export interface TaskDefinition { +export interface ITaskDefinition { extensionId: string; taskType: string; required: string[]; @@ -1035,8 +1062,8 @@ export class TaskSorter { } public compare(a: Task | ConfiguringTask, b: Task | ConfiguringTask): number { - let aw = a.getWorkspaceFolder(); - let bw = b.getWorkspaceFolder(); + const aw = a.getWorkspaceFolder(); + const bw = b.getWorkspaceFolder(); if (aw && bw) { let ai = this._order.get(aw.uri.toString()); ai = ai === undefined ? 0 : ai + 1; @@ -1076,7 +1103,7 @@ export const enum TaskRunType { Background = 'background' } -export interface TaskEvent { +export interface ITaskEvent { kind: TaskEventKind; taskId?: string; taskName?: string; @@ -1097,13 +1124,13 @@ export const enum TaskRunSource { } export namespace TaskEvent { - export function create(kind: TaskEventKind.ProcessStarted | TaskEventKind.ProcessEnded, task: Task, processIdOrExitCode?: number): TaskEvent; - export function create(kind: TaskEventKind.Start, task: Task, terminalId?: number, resolvedVariables?: Map<string, string>): TaskEvent; - export function create(kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Start | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.End, task: Task): TaskEvent; - export function create(kind: TaskEventKind.Changed): TaskEvent; - export function create(kind: TaskEventKind, task?: Task, processIdOrExitCodeOrTerminalId?: number, resolvedVariables?: Map<string, string>): TaskEvent { + export function create(kind: TaskEventKind.ProcessStarted | TaskEventKind.ProcessEnded, task: Task, processIdOrExitCode?: number): ITaskEvent; + export function create(kind: TaskEventKind.Start, task: Task, terminalId?: number, resolvedVariables?: Map<string, string>): ITaskEvent; + export function create(kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Start | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.End, task: Task): ITaskEvent; + export function create(kind: TaskEventKind.Changed): ITaskEvent; + export function create(kind: TaskEventKind, task?: Task, processIdOrExitCodeOrTerminalId?: number, resolvedVariables?: Map<string, string>): ITaskEvent { if (task) { - let result: TaskEvent = { + const result: ITaskEvent = { kind: kind, taskId: task._id, taskName: task.configurationProperties.name, @@ -1144,36 +1171,36 @@ export namespace KeyedTaskIdentifier { } return result; } - export function create(value: TaskIdentifier): KeyedTaskIdentifier { + export function create(value: ITaskIdentifier): KeyedTaskIdentifier { const resultKey = sortedStringify(value); - let result = { _key: resultKey, type: value.taskType }; + const result = { _key: resultKey, type: value.taskType }; Object.assign(result, value); return result; } } export namespace TaskDefinition { - export function createTaskIdentifier(external: TaskIdentifier, reporter: { error(message: string): void }): KeyedTaskIdentifier | undefined { - let definition = TaskDefinitionRegistry.get(external.type); + export function createTaskIdentifier(external: ITaskIdentifier, reporter: { error(message: string): void }): KeyedTaskIdentifier | undefined { + const definition = TaskDefinitionRegistry.get(external.type); if (definition === undefined) { // We have no task definition so we can't sanitize the literal. Take it as is - let copy = Objects.deepClone(external); + const copy = Objects.deepClone(external); delete copy._key; return KeyedTaskIdentifier.create(copy); } - let literal: { type: string;[name: string]: any } = Object.create(null); + const literal: { type: string;[name: string]: any } = Object.create(null); literal.type = definition.taskType; - let required: Set<string> = new Set(); + const required: Set<string> = new Set(); definition.required.forEach(element => required.add(element)); - let properties = definition.properties; - for (let property of Object.keys(properties)) { - let value = external[property]; + const properties = definition.properties; + for (const property of Object.keys(properties)) { + const value = external[property]; if (value !== undefined && value !== null) { literal[property] = value; } else if (required.has(property)) { - let schema = properties[property]; + const schema = properties[property]; if (schema.default !== undefined) { literal[property] = Objects.deepClone(schema.default); } else { diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index a17d4185861..0620c9bc2d5 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -10,7 +10,7 @@ import { ITaskSystem } from 'vs/workbench/contrib/tasks/common/taskSystem'; import { ExecutionEngine } from 'vs/workbench/contrib/tasks/common/tasks'; import * as TaskConfig from '../common/taskConfiguration'; import { AbstractTaskService } from 'vs/workbench/contrib/tasks/browser/abstractTaskService'; -import { TaskFilter, ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; +import { ITaskFilter, ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { TerminalTaskSystem } from 'vs/workbench/contrib/tasks/browser/terminalTaskSystem'; import { IConfirmationResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -43,10 +43,11 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; -interface WorkspaceFolderConfigurationResult { +interface IWorkspaceFolderConfigurationResult { workspaceFolder: IWorkspaceFolder; - config: TaskConfig.ExternalTaskRunnerConfiguration | undefined; + config: TaskConfig.IExternalTaskRunnerConfiguration | undefined; hasErrors: boolean; } @@ -83,7 +84,8 @@ export class TaskService extends AbstractTaskService { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IWorkspaceTrustRequestService workspaceTrustRequestService: IWorkspaceTrustRequestService, @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, - @ILogService logService: ILogService) { + @ILogService logService: ILogService, + @IThemeService themeService: IThemeService) { super(configurationService, markerService, outputService, @@ -115,15 +117,16 @@ export class TaskService extends AbstractTaskService { viewDescriptorService, workspaceTrustRequestService, workspaceTrustManagementService, - logService); + logService, + themeService); this._register(lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(), 'veto.tasks'))); } - protected getTaskSystem(): ITaskSystem { + protected _getTaskSystem(): ITaskSystem { if (this._taskSystem) { return this._taskSystem; } - this._taskSystem = this.createTerminalTaskSystem(); + this._taskSystem = this._createTerminalTaskSystem(); this._taskSystemListener = this._taskSystem!.onDidStateChange((event) => { if (this._taskSystem) { this._taskRunningState.set(this._taskSystem.isActiveSync()); @@ -133,8 +136,8 @@ export class TaskService extends AbstractTaskService { return this._taskSystem; } - protected computeLegacyConfiguration(workspaceFolder: IWorkspaceFolder): Promise<WorkspaceFolderConfigurationResult> { - let { config, hasParseErrors } = this.getConfiguration(workspaceFolder); + protected _computeLegacyConfiguration(workspaceFolder: IWorkspaceFolder): Promise<IWorkspaceFolderConfigurationResult> { + const { config, hasParseErrors } = this._getConfiguration(workspaceFolder); if (hasParseErrors) { return Promise.resolve({ workspaceFolder: workspaceFolder, hasErrors: true, config: undefined }); } @@ -145,9 +148,9 @@ export class TaskService extends AbstractTaskService { } } - protected versionAndEngineCompatible(filter?: TaskFilter): boolean { - let range = filter && filter.version ? filter.version : undefined; - let engine = this.executionEngine; + protected _versionAndEngineCompatible(filter?: ITaskFilter): boolean { + const range = filter && filter.version ? filter.version : undefined; + const engine = this.executionEngine; return (range === undefined) || ((semver.satisfies('0.1.0', range) && engine === ExecutionEngine.Process) || (semver.satisfies('2.0.0', range) && engine === ExecutionEngine.Terminal)); } @@ -169,7 +172,7 @@ export class TaskService extends AbstractTaskService { if (this._taskSystem.canAutoTerminate()) { terminatePromise = Promise.resolve({ confirmed: true }); } else { - terminatePromise = this.dialogService.confirm({ + terminatePromise = this._dialogService.confirm({ message: nls.localize('TaskSystem.runningTask', 'There is a task running. Do you want to terminate it?'), primaryButton: nls.localize({ key: 'TaskSystem.terminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task"), type: 'question' @@ -181,7 +184,7 @@ export class TaskService extends AbstractTaskService { return this._taskSystem!.terminateAll().then((responses) => { let success = true; let code: number | undefined = undefined; - for (let response of responses) { + for (const response of responses) { success = success && response.success; // We only have a code in the old output runner which only has one task // So we can use the first code. @@ -191,10 +194,10 @@ export class TaskService extends AbstractTaskService { } if (success) { this._taskSystem = undefined; - this.disposeTaskSystemListeners(); + this._disposeTaskSystemListeners(); return false; // no veto } else if (code && code === TerminateResponseCode.ProcessNotFound) { - return this.dialogService.confirm({ + return this._dialogService.confirm({ message: nls.localize('TaskSystem.noProcess', 'The launched task doesn\'t exist anymore. If the task spawned background processes exiting VS Code might result in orphaned processes. To avoid this start the last background process with a wait flag.'), primaryButton: nls.localize({ key: 'TaskSystem.exitAnyways', comment: ['&& denotes a mnemonic'] }, "&&Exit Anyways"), type: 'info' diff --git a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts new file mode 100644 index 00000000000..45b60a78489 --- /dev/null +++ b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ok } from 'assert'; +import { Emitter, Event } from 'vs/base/common/event'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ACTIVE_TASK_STATUS, FAILED_TASK_STATUS, SUCCEEDED_TASK_STATUS, TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTerminalStatus'; +import { AbstractProblemCollector } from 'vs/workbench/contrib/tasks/common/problemCollectors'; +import { CommonTask, ITaskEvent, TaskEventKind, TaskRunType } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskService, Task } from 'vs/workbench/contrib/tasks/common/taskService'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalStatus, ITerminalStatusList, TerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; + +class TestTaskService implements Partial<ITaskService> { + private readonly _onDidStateChange: Emitter<ITaskEvent> = new Emitter(); + public get onDidStateChange(): Event<ITaskEvent> { + return this._onDidStateChange.event; + } + public triggerStateChange(event: ITaskEvent): void { + this._onDidStateChange.fire(event); + } +} + +class TestTerminal implements Partial<ITerminalInstance> { + statusList: TerminalStatusList = new TerminalStatusList(new TestConfigurationService()); +} + +class TestTask extends CommonTask { + protected getFolderId(): string | undefined { + throw new Error('Method not implemented.'); + } + protected fromObject(object: any): Task { + throw new Error('Method not implemented.'); + } +} + +class TestProblemCollector implements Partial<AbstractProblemCollector> { + protected readonly _onDidFindFirstMatch = new Emitter<void>(); + readonly onDidFindFirstMatch = this._onDidFindFirstMatch.event; + protected readonly _onDidFindErrors = new Emitter<void>(); + readonly onDidFindErrors = this._onDidFindErrors.event; + protected readonly _onDidRequestInvalidateLastMarker = new Emitter<void>(); + readonly onDidRequestInvalidateLastMarker = this._onDidRequestInvalidateLastMarker.event; +} + +suite('Task Terminal Status', () => { + let instantiationService: TestInstantiationService; + let taskService: TestTaskService; + let taskTerminalStatus: TaskTerminalStatus; + let testTerminal: ITerminalInstance; + let testTask: Task; + let problemCollector: AbstractProblemCollector; + setup(() => { + instantiationService = new TestInstantiationService(); + taskService = new TestTaskService(); + taskTerminalStatus = instantiationService.createInstance(TaskTerminalStatus, taskService); + testTerminal = instantiationService.createInstance(TestTerminal); + testTask = instantiationService.createInstance(TestTask); + problemCollector = instantiationService.createInstance(TestProblemCollector); + }); + test('Should add failed status when there is an exit code on task end', async () => { + taskTerminalStatus.addTerminal(testTask, testTerminal, problemCollector); + taskService.triggerStateChange({ kind: TaskEventKind.ProcessStarted }); + assertStatus(testTerminal.statusList, ACTIVE_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.Inactive }); + assertStatus(testTerminal.statusList, SUCCEEDED_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.End, exitCode: 2 }); + await poll<void>(async () => Promise.resolve(), () => testTerminal?.statusList.primary?.id === FAILED_TASK_STATUS.id, 'terminal status should be updated'); + }); + test('Should add active status when a non-background task is run for a second time in the same terminal', () => { + taskTerminalStatus.addTerminal(testTask, testTerminal, problemCollector); + taskService.triggerStateChange({ kind: TaskEventKind.ProcessStarted }); + assertStatus(testTerminal.statusList, ACTIVE_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.Inactive }); + assertStatus(testTerminal.statusList, SUCCEEDED_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.ProcessStarted, runType: TaskRunType.SingleRun }); + assertStatus(testTerminal.statusList, ACTIVE_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.Inactive }); + assertStatus(testTerminal.statusList, SUCCEEDED_TASK_STATUS); + }); + test('Should drop status when a background task exits', async () => { + taskTerminalStatus.addTerminal(testTask, testTerminal, problemCollector); + taskService.triggerStateChange({ kind: TaskEventKind.ProcessStarted, runType: TaskRunType.Background }); + assertStatus(testTerminal.statusList, ACTIVE_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.Inactive }); + assertStatus(testTerminal.statusList, SUCCEEDED_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.ProcessEnded, exitCode: 0 }); + await poll<void>(async () => Promise.resolve(), () => testTerminal?.statusList.statuses?.includes(SUCCEEDED_TASK_STATUS) === false, 'terminal should have dropped status'); + }); + test('Should add succeeded status when a non-background task exits', () => { + taskTerminalStatus.addTerminal(testTask, testTerminal, problemCollector); + taskService.triggerStateChange({ kind: TaskEventKind.ProcessStarted, runType: TaskRunType.SingleRun }); + assertStatus(testTerminal.statusList, ACTIVE_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.Inactive }); + assertStatus(testTerminal.statusList, SUCCEEDED_TASK_STATUS); + taskService.triggerStateChange({ kind: TaskEventKind.ProcessEnded, exitCode: 0 }); + assertStatus(testTerminal.statusList, SUCCEEDED_TASK_STATUS); + }); +}); + +function assertStatus(actual: ITerminalStatusList, expected: ITerminalStatus): void { + ok(actual.statuses.length === 1, '# of statuses'); + ok(actual.primary?.id === expected.id, 'ID'); + ok(actual.primary?.severity === expected.severity, 'Severity'); +} + +async function poll<T>( + fn: () => Thenable<T>, + acceptFn: (result: T) => boolean, + timeoutMessage: string, + retryCount: number = 200, + retryInterval: number = 10 // millis +): Promise<T> { + let trial = 1; + let lastError: string = ''; + + while (true) { + if (trial > retryCount) { + throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.\r${lastError}`); + } + + let result; + try { + result = await fn(); + if (acceptFn(result)) { + return result; + } else { + lastError = 'Did not pass accept function'; + } + } catch (e: any) { + lastError = Array.isArray(e.stack) ? e.stack.join('\n') : e.stack; + } + + await new Promise(resolve => setTimeout(resolve, retryInterval)); + trial++; + } +} diff --git a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts index 2de0e6c6100..6b41d88c5f7 100644 --- a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts @@ -67,10 +67,10 @@ suite('ProblemPatternParser', () => { suite('single-pattern definitions', () => { test('parses a pattern defined by only a regexp', () => { - let problemPattern: matchers.Config.ProblemPattern = { + const problemPattern: matchers.Config.IProblemPattern = { regexp: 'test' }; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert(reporter.isOK()); assert.deepStrictEqual(parsed, { regexp: testRegexp, @@ -82,11 +82,11 @@ suite('ProblemPatternParser', () => { }); }); test('does not sets defaults for line and character if kind is File', () => { - let problemPattern: matchers.Config.ProblemPattern = { + const problemPattern: matchers.Config.IProblemPattern = { regexp: 'test', kind: 'file' }; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.deepStrictEqual(parsed, { regexp: testRegexp, kind: matchers.ProblemLocationKind.File, @@ -98,10 +98,10 @@ suite('ProblemPatternParser', () => { suite('multi-pattern definitions', () => { test('defines a pattern based on regexp and property fields, with file/line location', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 3, line: 4, column: 5, message: 6 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert(reporter.isOK()); assert.deepStrictEqual(parsed, [{ @@ -115,10 +115,10 @@ suite('ProblemPatternParser', () => { ); }); test('defines a pattern bsaed on regexp and property fields, with location', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 3, location: 4, message: 6 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert(reporter.isOK()); assert.deepStrictEqual(parsed, [{ @@ -131,13 +131,13 @@ suite('ProblemPatternParser', () => { ); }); test('accepts a pattern that provides the fields from multiple entries', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 3 }, { regexp: 'test1', line: 4 }, { regexp: 'test2', column: 5 }, { regexp: 'test3', message: 6 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert(reporter.isOK()); assert.deepStrictEqual(parsed, [ { regexp: testRegexp, kind: matchers.ProblemLocationKind.Location, file: 3 }, @@ -147,82 +147,82 @@ suite('ProblemPatternParser', () => { ]); }); test('forbids setting the loop flag outside of the last element in the array', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 3, loop: true }, { regexp: 'test1', line: 4 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The loop property is only supported on the last line matcher.')); }); test('forbids setting the kind outside of the first element of the array', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 3 }, { regexp: 'test1', kind: 'file', line: 4 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The problem pattern is invalid. The kind property must be provided only in the first element')); }); test('kind: Location requires a regexp', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { file: 0, line: 1, column: 20, message: 0 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The problem pattern is missing a regular expression.')); }); test('kind: Location requires a regexp on every entry', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 3 }, { line: 4 }, { regexp: 'test2', column: 5 }, { regexp: 'test3', message: 6 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The problem pattern is missing a regular expression.')); }); test('kind: Location requires a message', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 0, line: 1, column: 20 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The problem pattern is invalid. It must have at least have a file and a message.')); }); test('kind: Location requires a file', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', line: 1, column: 20, message: 0 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The problem pattern is invalid. It must either have kind: "file" or have a line or location match group.')); }); test('kind: Location requires either a line or location', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 1, column: 20, message: 0 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The problem pattern is invalid. It must either have kind: "file" or have a line or location match group.')); }); test('kind: File accepts a regexp, file and message', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', file: 2, kind: 'file', message: 6 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert(reporter.isOK()); assert.deepStrictEqual(parsed, [{ @@ -235,20 +235,20 @@ suite('ProblemPatternParser', () => { }); test('kind: File requires a file', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', kind: 'file', message: 6 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The problem pattern is invalid. It must have at least have a file and a message.')); }); test('kind: File requires a message', () => { - let problemPattern: matchers.Config.MultiLineProblemPattern = [ + const problemPattern: matchers.Config.MultiLineProblemPattern = [ { regexp: 'test', kind: 'file', file: 6 } ]; - let parsed = parser.parse(problemPattern); + const parsed = parser.parse(problemPattern); assert.strictEqual(null, parsed); assert.strictEqual(ValidationState.Error, reporter.state); assert(reporter.hasMessage('The problem pattern is invalid. It must have at least have a file and a message.')); diff --git a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts index c1ef36f8b17..3be96c81dba 100644 --- a/src/vs/workbench/contrib/tasks/test/common/configuration.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/taskConfiguration.test.ts @@ -10,14 +10,16 @@ import * as UUID from 'vs/base/common/uuid'; import * as Types from 'vs/base/common/types'; import * as Platform from 'vs/base/common/platform'; import { ValidationStatus } from 'vs/base/common/parsers'; -import { ProblemMatcher, FileLocationKind, ProblemPattern, ApplyToKind } from 'vs/workbench/contrib/tasks/common/problemMatcher'; +import { ProblemMatcher, FileLocationKind, IProblemPattern, ApplyToKind, INamedProblemMatcher } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { WorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace'; import * as Tasks from 'vs/workbench/contrib/tasks/common/tasks'; -import { parse, ParseResult, IProblemReporter, ExternalTaskRunnerConfiguration, CustomTask, TaskConfigSource } from 'vs/workbench/contrib/tasks/common/taskConfiguration'; +import { parse, IParseResult, IProblemReporter, IExternalTaskRunnerConfiguration, ICustomTask, TaskConfigSource, IParseContext, ProblemMatcherConverter, IGlobals, ITaskParseResult, UUIDMap, TaskParser } from 'vs/workbench/contrib/tasks/common/taskConfiguration'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContext } from 'vs/platform/contextkey/common/contextkey'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ITaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; const workspaceFolder: WorkspaceFolder = new WorkspaceFolder({ uri: URI.file('/workspace/folderOne'), @@ -58,6 +60,10 @@ class ProblemReporter implements IProblemReporter { this.receivedMessage = true; this.lastMessage = message; } + + public clearMessage(): void { + this.lastMessage = undefined; + } } class ConfiguationBuilder { @@ -71,14 +77,14 @@ class ConfiguationBuilder { } public task(name: string, command: string): CustomTaskBuilder { - let builder = new CustomTaskBuilder(this, name, command); + const builder = new CustomTaskBuilder(this, name, command); this.builders.push(builder); this.result.push(builder.result); return builder; } public done(): void { - for (let builder of this.builders) { + for (const builder of this.builders) { builder.done(); } } @@ -86,7 +92,7 @@ class ConfiguationBuilder { class PresentationBuilder { - public result: Tasks.PresentationOptions; + public result: Tasks.IPresentationOptions; constructor(public parent: CommandConfigurationBuilder) { this.result = { echo: false, reveal: Tasks.RevealKind.Always, revealProblems: Tasks.RevealProblemKind.Never, focus: false, panel: Tasks.PanelKind.Shared, showReuseMessage: true, clear: false, close: false }; @@ -127,7 +133,7 @@ class PresentationBuilder { } class CommandConfigurationBuilder { - public result: Tasks.CommandConfiguration; + public result: Tasks.ICommandConfiguration; private presentationBuilder: PresentationBuilder; @@ -231,7 +237,7 @@ class CustomTaskBuilder { } public problemMatcher(): ProblemMatcherBuilder { - let builder = new ProblemMatcherBuilder(this); + const builder = new ProblemMatcherBuilder(this); this.result.configurationProperties.problemMatchers!.push(builder.result); return builder; } @@ -288,7 +294,7 @@ class ProblemMatcherBuilder { } public pattern(regExp: RegExp): PatternBuilder { - let builder = new PatternBuilder(this, regExp); + const builder = new PatternBuilder(this, regExp); if (!this.result.pattern) { this.result.pattern = builder.result; } @@ -297,7 +303,7 @@ class ProblemMatcherBuilder { } class PatternBuilder { - public result: ProblemPattern; + public result: IProblemPattern; constructor(public parent: ProblemMatcherBuilder, regExp: RegExp) { this.result = { @@ -370,20 +376,20 @@ class TasksMockContextKeyService extends MockContextKeyService { } } -function testDefaultProblemMatcher(external: ExternalTaskRunnerConfiguration, resolved: number) { - let reporter = new ProblemReporter(); - let result = parse(workspaceFolder, workspace, Platform.platform, external, reporter, TaskConfigSource.TasksJson, new TasksMockContextKeyService()); +function testDefaultProblemMatcher(external: IExternalTaskRunnerConfiguration, resolved: number) { + const reporter = new ProblemReporter(); + const result = parse(workspaceFolder, workspace, Platform.platform, external, reporter, TaskConfigSource.TasksJson, new TasksMockContextKeyService()); assert.ok(!reporter.receivedMessage); assert.strictEqual(result.custom.length, 1); - let task = result.custom[0]; + const task = result.custom[0]; assert.ok(task); assert.strictEqual(task.configurationProperties.problemMatchers!.length, resolved); } -function testConfiguration(external: ExternalTaskRunnerConfiguration, builder: ConfiguationBuilder): void { +function testConfiguration(external: IExternalTaskRunnerConfiguration, builder: ConfiguationBuilder): void { builder.done(); - let reporter = new ProblemReporter(); - let result = parse(workspaceFolder, workspace, Platform.platform, external, reporter, TaskConfigSource.TasksJson, new TasksMockContextKeyService()); + const reporter = new ProblemReporter(); + const result = parse(workspaceFolder, workspace, Platform.platform, external, reporter, TaskConfigSource.TasksJson, new TasksMockContextKeyService()); if (reporter.receivedMessage) { assert.ok(false, reporter.lastMessage); } @@ -407,8 +413,8 @@ class TaskGroupMap { } public static assert(actual: TaskGroupMap, expected: TaskGroupMap): void { - let actualKeys = Object.keys(actual._store); - let expectedKeys = Object.keys(expected._store); + const actualKeys = Object.keys(actual._store); + const expectedKeys = Object.keys(expected._store); if (actualKeys.length === 0 && expectedKeys.length === 0) { return; } @@ -416,14 +422,14 @@ class TaskGroupMap { actualKeys.forEach(key => assert.ok(expected._store[key])); expectedKeys.forEach(key => actual._store[key]); actualKeys.forEach((key) => { - let actualTasks = actual._store[key]; - let expectedTasks = expected._store[key]; + const actualTasks = actual._store[key]; + const expectedTasks = expected._store[key]; assert.strictEqual(actualTasks.length, expectedTasks.length); if (actualTasks.length === 1) { assert.strictEqual(actualTasks[0].configurationProperties.name, expectedTasks[0].configurationProperties.name); return; } - let expectedTaskMap: { [key: string]: boolean } = Object.create(null); + const expectedTaskMap: { [key: string]: boolean } = Object.create(null); expectedTasks.forEach(task => expectedTaskMap[task.configurationProperties.name!] = true); actualTasks.forEach(task => delete expectedTaskMap[task.configurationProperties.name!]); assert.strictEqual(Object.keys(expectedTaskMap).length, 0); @@ -431,9 +437,9 @@ class TaskGroupMap { } } -function assertConfiguration(result: ParseResult, expected: Tasks.Task[]): void { +function assertConfiguration(result: IParseResult, expected: Tasks.Task[]): void { assert.ok(result.validationStatus.isOK()); - let actual = result.custom; + const actual = result.custom; assert.strictEqual(typeof actual, typeof expected); if (!actual) { return; @@ -441,34 +447,34 @@ function assertConfiguration(result: ParseResult, expected: Tasks.Task[]): void // We can't compare Ids since the parser uses UUID which are random // So create a new map using the name. - let actualTasks: { [key: string]: Tasks.Task } = Object.create(null); - let actualId2Name: { [key: string]: string } = Object.create(null); - let actualTaskGroups = new TaskGroupMap(); + const actualTasks: { [key: string]: Tasks.Task } = Object.create(null); + const actualId2Name: { [key: string]: string } = Object.create(null); + const actualTaskGroups = new TaskGroupMap(); actual.forEach(task => { assert.ok(!actualTasks[task.configurationProperties.name!]); actualTasks[task.configurationProperties.name!] = task; actualId2Name[task._id] = task.configurationProperties.name!; - let taskId = Tasks.TaskGroup.from(task.configurationProperties.group)?._id; + const taskId = Tasks.TaskGroup.from(task.configurationProperties.group)?._id; if (taskId) { actualTaskGroups.add(taskId, task); } }); - let expectedTasks: { [key: string]: Tasks.Task } = Object.create(null); - let expectedTaskGroup = new TaskGroupMap(); + const expectedTasks: { [key: string]: Tasks.Task } = Object.create(null); + const expectedTaskGroup = new TaskGroupMap(); expected.forEach(task => { assert.ok(!expectedTasks[task.configurationProperties.name!]); expectedTasks[task.configurationProperties.name!] = task; - let taskId = Tasks.TaskGroup.from(task.configurationProperties.group)?._id; + const taskId = Tasks.TaskGroup.from(task.configurationProperties.group)?._id; if (taskId) { expectedTaskGroup.add(taskId, task); } }); - let actualKeys = Object.keys(actualTasks); + const actualKeys = Object.keys(actualTasks); assert.strictEqual(actualKeys.length, expected.length); actualKeys.forEach((key) => { - let actualTask = actualTasks[key]; - let expectedTask = expectedTasks[key]; + const actualTask = actualTasks[key]; + const expectedTask = expectedTasks[key]; assert.ok(expectedTask); assertTask(actualTask, expectedTask); }); @@ -502,7 +508,7 @@ function assertTask(actual: Tasks.Task, expected: Tasks.Task) { } } -function assertCommandConfiguration(actual: Tasks.CommandConfiguration, expected: Tasks.CommandConfiguration) { +function assertCommandConfiguration(actual: Tasks.ICommandConfiguration, expected: Tasks.ICommandConfiguration) { assert.strictEqual(typeof actual, typeof expected); if (actual && expected) { assertPresentation(actual.presentation!, expected.presentation!); @@ -530,7 +536,7 @@ function assertGroup(actual: Tasks.TaskGroup, expected: Tasks.TaskGroup) { } } -function assertPresentation(actual: Tasks.PresentationOptions, expected: Tasks.PresentationOptions) { +function assertPresentation(actual: Tasks.IPresentationOptions, expected: Tasks.IPresentationOptions) { assert.strictEqual(typeof actual, typeof expected); if (actual && expected) { assert.strictEqual(actual.echo, expected.echo); @@ -560,21 +566,21 @@ function assertProblemMatcher(actual: string | ProblemMatcher, expected: string } } -function assertProblemPatterns(actual: ProblemPattern | ProblemPattern[], expected: ProblemPattern | ProblemPattern[]) { +function assertProblemPatterns(actual: IProblemPattern | IProblemPattern[], expected: IProblemPattern | IProblemPattern[]) { assert.strictEqual(typeof actual, typeof expected); if (Array.isArray(actual)) { - let actuals = <ProblemPattern[]>actual; - let expecteds = <ProblemPattern[]>expected; + const actuals = <IProblemPattern[]>actual; + const expecteds = <IProblemPattern[]>expected; assert.strictEqual(actuals.length, expecteds.length); for (let i = 0; i < actuals.length; i++) { assertProblemPattern(actuals[i], expecteds[i]); } } else { - assertProblemPattern(<ProblemPattern>actual, <ProblemPattern>expected); + assertProblemPattern(<IProblemPattern>actual, <IProblemPattern>expected); } } -function assertProblemPattern(actual: ProblemPattern, expected: ProblemPattern) { +function assertProblemPattern(actual: IProblemPattern, expected: IProblemPattern) { assert.strictEqual(actual.regexp.toString(), expected.regexp.toString()); assert.strictEqual(actual.file, expected.file); assert.strictEqual(actual.message, expected.message); @@ -593,7 +599,7 @@ function assertProblemPattern(actual: ProblemPattern, expected: ProblemPattern) suite('Tasks version 0.1.0', () => { test('tasks: all default', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true); @@ -605,7 +611,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global isShellCommand', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true). @@ -620,7 +626,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global show output silent', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder. task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). @@ -637,7 +643,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global promptOnClose default', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true); @@ -652,7 +658,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global promptOnClose', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). promptOnClose(false). @@ -668,7 +674,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global promptOnClose default watching', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). isBackground(true). @@ -685,7 +691,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global show output never', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder. task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). @@ -702,7 +708,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global echo Command', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder. task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). @@ -720,7 +726,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global args', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder. task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). @@ -739,7 +745,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: options - cwd', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder. task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). @@ -760,7 +766,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: options - env', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder. task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). @@ -781,13 +787,13 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: os windows', () => { - let name: string = Platform.isWindows ? 'tsc.win' : 'tsc'; - let builder = new ConfiguationBuilder(); + const name: string = Platform.isWindows ? 'tsc.win' : 'tsc'; + const builder = new ConfiguationBuilder(); builder. task(name, name). group(Tasks.TaskGroup.Build). command().suppressTaskName(true); - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', windows: { @@ -798,14 +804,14 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: os windows & global isShellCommand', () => { - let name: string = Platform.isWindows ? 'tsc.win' : 'tsc'; - let builder = new ConfiguationBuilder(); + const name: string = Platform.isWindows ? 'tsc.win' : 'tsc'; + const builder = new ConfiguationBuilder(); builder. task(name, name). group(Tasks.TaskGroup.Build). command().suppressTaskName(true). runtime(Tasks.RuntimeType.Shell); - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', isShellCommand: true, @@ -817,13 +823,13 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: os mac', () => { - let name: string = Platform.isMacintosh ? 'tsc.osx' : 'tsc'; - let builder = new ConfiguationBuilder(); + const name: string = Platform.isMacintosh ? 'tsc.osx' : 'tsc'; + const builder = new ConfiguationBuilder(); builder. task(name, name). group(Tasks.TaskGroup.Build). command().suppressTaskName(true); - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', osx: { @@ -834,13 +840,13 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: os linux', () => { - let name: string = Platform.isLinux ? 'tsc.linux' : 'tsc'; - let builder = new ConfiguationBuilder(); + const name: string = Platform.isLinux ? 'tsc.linux' : 'tsc'; + const builder = new ConfiguationBuilder(); builder. task(name, name). group(Tasks.TaskGroup.Build). command().suppressTaskName(true); - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', linux: { @@ -851,13 +857,13 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: overwrite showOutput', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder. task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true). presentation().reveal(Platform.isWindows ? Tasks.RevealKind.Always : Tasks.RevealKind.Never); - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', showOutput: 'never', @@ -869,14 +875,14 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: overwrite echo Command', () => { - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder. task('tsc', 'tsc'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true). presentation(). echo(Platform.isWindows ? false : true); - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', echoCommand: true, @@ -888,7 +894,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global problemMatcher one', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', problemMatcher: '$msCompile' @@ -897,7 +903,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: global problemMatcher two', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', problemMatcher: ['$eslint-compact', '$msCompile'] @@ -906,7 +912,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: task definition', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -915,29 +921,29 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc').command().args(['$name']); testConfiguration(external, builder); }); test('tasks: build task', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ { taskName: 'taskName', isBuildCommand: true - } as CustomTask + } as ICustomTask ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc').group(Tasks.TaskGroup.Build).command().args(['$name']); testConfiguration(external, builder); }); test('tasks: default build task', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -946,29 +952,29 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('build', 'tsc').group(Tasks.TaskGroup.Build).command().args(['$name']); testConfiguration(external, builder); }); test('tasks: test task', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ { taskName: 'taskName', isTestCommand: true - } as CustomTask + } as ICustomTask ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc').group(Tasks.TaskGroup.Test).command().args(['$name']); testConfiguration(external, builder); }); test('tasks: default test task', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -977,13 +983,13 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('test', 'tsc').group(Tasks.TaskGroup.Test).command().args(['$name']); testConfiguration(external, builder); }); test('tasks: task with values', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -993,10 +999,10 @@ suite('Tasks version 0.1.0', () => { echoCommand: true, args: ['--p'], isWatching: true - } as CustomTask + } as ICustomTask ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('test', 'tsc'). group(Tasks.TaskGroup.Test). isBackground(true). @@ -1009,7 +1015,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: task inherits global values', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', showOutput: 'never', @@ -1020,7 +1026,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('test', 'tsc'). group(Tasks.TaskGroup.Test). command().args(['$name']).presentation(). @@ -1030,7 +1036,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: problem matcher default', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1044,7 +1050,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command().args(['$name']).parent. problemMatcher().pattern(/abc/); @@ -1052,7 +1058,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: problem matcher .* regular expression', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1066,7 +1072,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command().args(['$name']).parent. problemMatcher().pattern(/.*/); @@ -1074,7 +1080,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: problem matcher owner, applyTo, severity and fileLocation', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1092,7 +1098,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command().args(['$name']).parent. problemMatcher(). @@ -1106,7 +1112,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: problem matcher fileLocation and filePrefix', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1121,7 +1127,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command().args(['$name']).parent. problemMatcher(). @@ -1132,7 +1138,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: problem pattern location', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1151,7 +1157,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command().args(['$name']).parent. problemMatcher(). @@ -1160,7 +1166,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: problem pattern line & column', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1182,7 +1188,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command().args(['$name']).parent. problemMatcher(). @@ -1193,7 +1199,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: prompt on close default', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1202,7 +1208,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). promptOnClose(true). command().args(['$name']); @@ -1210,17 +1216,17 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: prompt on close watching', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ { taskName: 'taskName', isWatching: true - } as CustomTask + } as ICustomTask ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). isBackground(true).promptOnClose(false). command().args(['$name']); @@ -1228,7 +1234,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: prompt on close set', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1238,7 +1244,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). promptOnClose(false). command().args(['$name']); @@ -1246,7 +1252,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: task selector set', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', taskSelector: '/t:', @@ -1256,7 +1262,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command(). taskSelector('/t:'). @@ -1265,7 +1271,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: suppress task name set', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', suppressTaskName: false, @@ -1273,17 +1279,17 @@ suite('Tasks version 0.1.0', () => { { taskName: 'taskName', suppressTaskName: true - } as CustomTask + } as ICustomTask ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command().suppressTaskName(true); testConfiguration(external, builder); }); test('tasks: suppress task name inherit', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', suppressTaskName: true, @@ -1293,14 +1299,14 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskName', 'tsc'). command().suppressTaskName(true); testConfiguration(external, builder); }); test('tasks: two tasks', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ @@ -1312,7 +1318,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskNameOne', 'tsc'). command().args(['$name']); builder.task('taskNameTwo', 'tsc'). @@ -1321,7 +1327,7 @@ suite('Tasks version 0.1.0', () => { }); test('tasks: with command', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', tasks: [ { @@ -1330,13 +1336,13 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskNameOne', 'tsc').command().suppressTaskName(true); testConfiguration(external, builder); }); test('tasks: two tasks with command', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', tasks: [ { @@ -1349,14 +1355,14 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskNameOne', 'tsc').command().suppressTaskName(true); builder.task('taskNameTwo', 'dir').command().suppressTaskName(true); testConfiguration(external, builder); }); test('tasks: with command and args', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', tasks: [ { @@ -1370,18 +1376,18 @@ suite('Tasks version 0.1.0', () => { env: 'env' } } - } as CustomTask + } as ICustomTask ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskNameOne', 'tsc').command().suppressTaskName(true). runtime(Tasks.RuntimeType.Shell).args(['arg']).options({ cwd: 'cwd', env: { env: 'env' } }); testConfiguration(external, builder); }); test('tasks: with command os specific', () => { - let name: string = Platform.isWindows ? 'tsc.win' : 'tsc'; - let external: ExternalTaskRunnerConfiguration = { + const name: string = Platform.isWindows ? 'tsc.win' : 'tsc'; + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', tasks: [ { @@ -1393,14 +1399,14 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskNameOne', name).command().suppressTaskName(true); testConfiguration(external, builder); }); test('tasks: with Windows specific args', () => { - let args: string[] = Platform.isWindows ? ['arg1', 'arg2'] : ['arg1']; - let external: ExternalTaskRunnerConfiguration = { + const args: string[] = Platform.isWindows ? ['arg1', 'arg2'] : ['arg1']; + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', tasks: [ { @@ -1413,14 +1419,14 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('tsc', 'tsc').command().suppressTaskName(true).args(args); testConfiguration(external, builder); }); test('tasks: with Linux specific args', () => { - let args: string[] = Platform.isLinux ? ['arg1', 'arg2'] : ['arg1']; - let external: ExternalTaskRunnerConfiguration = { + const args: string[] = Platform.isLinux ? ['arg1', 'arg2'] : ['arg1']; + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', tasks: [ { @@ -1433,29 +1439,29 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('tsc', 'tsc').command().suppressTaskName(true).args(args); testConfiguration(external, builder); }); test('tasks: global command and task command properties', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', tasks: [ { taskName: 'taskNameOne', isShellCommand: true, - } as CustomTask + } as ICustomTask ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskNameOne', 'tsc').command().runtime(Tasks.RuntimeType.Shell).args(['$name']); testConfiguration(external, builder); }); test('tasks: global and tasks args', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', args: ['global'], @@ -1466,13 +1472,13 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskNameOne', 'tsc').command().args(['global', '$name', 'local']); testConfiguration(external, builder); }); test('tasks: global and tasks args with task selector', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', command: 'tsc', args: ['global'], @@ -1484,7 +1490,7 @@ suite('Tasks version 0.1.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('taskNameOne', 'tsc').command().taskSelector('/t:').args(['global', '/t:taskNameOne', 'local']); testConfiguration(external, builder); }); @@ -1492,7 +1498,7 @@ suite('Tasks version 0.1.0', () => { suite('Tasks version 2.0.0', () => { test.skip('Build workspace task', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '2.0.0', tasks: [ { @@ -1503,7 +1509,7 @@ suite('Tasks version 2.0.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('dir', 'dir'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true). @@ -1512,13 +1518,13 @@ suite('Tasks version 2.0.0', () => { testConfiguration(external, builder); }); test('Global group none', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '2.0.0', command: 'dir', type: 'shell', group: 'none' }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('dir', 'dir'). command().suppressTaskName(true). runtime(Tasks.RuntimeType.Shell). @@ -1526,13 +1532,13 @@ suite('Tasks version 2.0.0', () => { testConfiguration(external, builder); }); test.skip('Global group build', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '2.0.0', command: 'dir', type: 'shell', group: 'build' }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('dir', 'dir'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true). @@ -1541,14 +1547,14 @@ suite('Tasks version 2.0.0', () => { testConfiguration(external, builder); }); test.skip('Global group default build', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '2.0.0', command: 'dir', type: 'shell', group: { kind: 'build', isDefault: true } }; - let builder = new ConfiguationBuilder(); - let taskGroup = Tasks.TaskGroup.Build; + const builder = new ConfiguationBuilder(); + const taskGroup = Tasks.TaskGroup.Build; taskGroup.isDefault = true; builder.task('dir', 'dir'). group(taskGroup). @@ -1558,7 +1564,7 @@ suite('Tasks version 2.0.0', () => { testConfiguration(external, builder); }); test('Local group none', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '2.0.0', tasks: [ { @@ -1569,7 +1575,7 @@ suite('Tasks version 2.0.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('dir', 'dir'). command().suppressTaskName(true). runtime(Tasks.RuntimeType.Shell). @@ -1577,7 +1583,7 @@ suite('Tasks version 2.0.0', () => { testConfiguration(external, builder); }); test.skip('Local group build', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '2.0.0', tasks: [ { @@ -1588,7 +1594,7 @@ suite('Tasks version 2.0.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('dir', 'dir'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true). @@ -1597,7 +1603,7 @@ suite('Tasks version 2.0.0', () => { testConfiguration(external, builder); }); test.skip('Local group default build', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '2.0.0', tasks: [ { @@ -1608,8 +1614,8 @@ suite('Tasks version 2.0.0', () => { } ] }; - let builder = new ConfiguationBuilder(); - let taskGroup = Tasks.TaskGroup.Build; + const builder = new ConfiguationBuilder(); + const taskGroup = Tasks.TaskGroup.Build; taskGroup.isDefault = true; builder.task('dir', 'dir'). group(taskGroup). @@ -1619,7 +1625,7 @@ suite('Tasks version 2.0.0', () => { testConfiguration(external, builder); }); test('Arg overwrite', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '2.0.0', tasks: [ { @@ -1647,7 +1653,7 @@ suite('Tasks version 2.0.0', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); if (Platform.isWindows) { builder.task('echo', 'echo'). command().suppressTaskName(true).args(['windows']). @@ -1672,7 +1678,7 @@ suite('Tasks version 2.0.0', () => { suite('Bugs / regression tests', () => { (Platform.isLinux ? test.skip : test)('Bug 19548', () => { - let external: ExternalTaskRunnerConfiguration = { + const external: IExternalTaskRunnerConfiguration = { version: '0.1.0', windows: { command: 'powershell', @@ -1694,7 +1700,7 @@ suite('Bugs / regression tests', () => { isBuildCommand: false, showOutput: 'always', echoCommand: true - } as CustomTask + } as ICustomTask ] }, osx: { @@ -1712,11 +1718,11 @@ suite('Bugs / regression tests', () => { ], isBuildCommand: false, showOutput: 'always' - } as CustomTask + } as ICustomTask ] } }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); if (Platform.isWindows) { builder.task('composeForDebug', 'powershell'). command().suppressTaskName(true). @@ -1735,7 +1741,7 @@ suite('Bugs / regression tests', () => { }); test('Bug 28489', () => { - let external = { + const external = { version: '0.1.0', command: '', isShellCommand: true, @@ -1751,7 +1757,7 @@ suite('Bugs / regression tests', () => { } ] }; - let builder = new ConfiguationBuilder(); + const builder = new ConfiguationBuilder(); builder.task('build', 'bash'). group(Tasks.TaskGroup.Build). command().suppressTaskName(true). @@ -1760,3 +1766,124 @@ suite('Bugs / regression tests', () => { testConfiguration(external, builder); }); }); + +class TestNamedProblemMatcher implements Partial<ProblemMatcher> { +} + +class TestParseContext implements Partial<IParseContext> { +} + +class TestTaskDefinitionRegistry implements Partial<ITaskDefinitionRegistry> { + private _task: Tasks.ITaskDefinition | undefined; + public get(key: string): Tasks.ITaskDefinition { + return this._task!; + } + public set(task: Tasks.ITaskDefinition) { + this._task = task; + } +} + +suite('Task configuration conversions', () => { + const globals = {} as IGlobals; + const taskConfigSource = {} as TaskConfigSource; + const TaskDefinitionRegistry = new TestTaskDefinitionRegistry(); + let instantiationService: TestInstantiationService; + let parseContext: IParseContext; + let namedProblemMatcher: INamedProblemMatcher; + let problemReporter: ProblemReporter; + setup(() => { + instantiationService = new TestInstantiationService(); + namedProblemMatcher = instantiationService.createInstance(TestNamedProblemMatcher); + namedProblemMatcher.name = 'real'; + namedProblemMatcher.label = 'real label'; + problemReporter = new ProblemReporter(); + parseContext = instantiationService.createInstance(TestParseContext); + parseContext.problemReporter = problemReporter; + parseContext.namedProblemMatchers = { 'real': namedProblemMatcher }; + parseContext.uuidMap = new UUIDMap(); + }); + suite('ProblemMatcherConverter.from', () => { + test('returns [] and an error for an unknown problem matcher', () => { + const result = (ProblemMatcherConverter.from('$fake', parseContext)); + assert.deepEqual(result.value, []); + assert.strictEqual(result.errors?.length, 1); + }); + test('returns config for a known problem matcher', () => { + const result = (ProblemMatcherConverter.from('$real', parseContext)); + assert.strictEqual(result.errors?.length, 0); + assert.deepEqual(result.value, [{ "label": "real label" }]); + }); + test('returns config for a known problem matcher including applyTo', () => { + namedProblemMatcher.applyTo = ApplyToKind.closedDocuments; + const result = (ProblemMatcherConverter.from('$real', parseContext)); + assert.strictEqual(result.errors?.length, 0); + assert.deepEqual(result.value, [{ "label": "real label", "applyTo": ApplyToKind.closedDocuments }]); + }); + }); + suite('TaskParser.from', () => { + suite('CustomTask', () => { + suite('incomplete config reports an appropriate error for missing', () => { + test('name', () => { + const result = TaskParser.from([{} as ICustomTask], globals, parseContext, taskConfigSource); + assertTaskParseResult(result, undefined, problemReporter, 'Error: a task must provide a label property'); + }); + test('command', () => { + const result = TaskParser.from([{ taskName: 'task' } as ICustomTask], globals, parseContext, taskConfigSource); + assertTaskParseResult(result, undefined, problemReporter, "Error: the task 'task' doesn't define a command"); + }); + }); + test('returns expected result', () => { + const expected = [ + { taskName: 'task', command: 'echo test' } as ICustomTask, + { taskName: 'task 2', command: 'echo test' } as ICustomTask + ]; + const result = TaskParser.from(expected, globals, parseContext, taskConfigSource); + assertTaskParseResult(result, { custom: expected }, problemReporter, undefined); + }); + }); + suite('ConfiguredTask', () => { + test('returns expected result', () => { + const expected = [{ taskName: 'task', command: 'echo test', type: 'any', label: 'task' }, { taskName: 'task 2', command: 'echo test', type: 'any', label: 'task 2' }]; + TaskDefinitionRegistry.set({ extensionId: 'registered', taskType: 'any', properties: {} } as Tasks.ITaskDefinition); + const result = TaskParser.from(expected, globals, parseContext, taskConfigSource, TaskDefinitionRegistry); + assertTaskParseResult(result, { configured: expected }, problemReporter, undefined); + }); + }); + }); +}); + +function assertTaskParseResult(actual: ITaskParseResult, expected: ITestTaskParseResult | undefined, problemReporter: ProblemReporter, expectedMessage?: string): void { + if (expectedMessage === undefined) { + assert.strictEqual(problemReporter.lastMessage, undefined); + } else { + assert.ok(problemReporter.lastMessage?.includes(expectedMessage)); + } + + assert.deepEqual(actual.custom.length, expected?.custom?.length || 0); + assert.deepEqual(actual.configured.length, expected?.configured?.length || 0); + + let index = 0; + if (expected?.configured) { + for (const taskParseResult of expected?.configured) { + assert.strictEqual(actual.configured[index]._label, taskParseResult.label); + index++; + } + } + index = 0; + if (expected?.custom) { + for (const taskParseResult of expected?.custom) { + assert.strictEqual(actual.custom[index]._label, taskParseResult.taskName); + index++; + } + } + problemReporter.clearMessage(); +} + +interface ITestTaskParseResult { + custom?: Partial<ICustomTask>[]; + configured?: Partial<ITestConfiguringTask>[]; +} + +interface ITestConfiguringTask extends Partial<Tasks.ConfiguringTask> { + label: string; +} diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index adf6f60619f..dfcf34f405d 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -25,6 +25,7 @@ import { getMimeTypes } from 'vs/editor/common/services/languagesAssociations'; import { hash } from 'vs/base/common/hash'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; type TelemetryData = { mimeType: string; @@ -54,7 +55,8 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr @IEditorService editorService: IEditorService, @IKeybindingService keybindingsService: IKeybindingService, @IWorkbenchThemeService themeService: IWorkbenchThemeService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IConfigurationService configurationService: IConfigurationService, @IPaneCompositePartService paneCompositeService: IPaneCompositePartService, @ITextFileService textFileService: ITextFileService @@ -72,6 +74,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr }; type WorkspaceLoadClassification = { + owner: 'bpasero'; userAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; emptyWorkbench: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; windowSize: WindowSizeFragment; @@ -134,12 +137,15 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr const settingsType = this.getTypeIfSettings(e.model.resource); if (settingsType) { type SettingsReadClassification = { + owner: 'bpasero'; settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data } else { - type FileGetClassification = {} & FileTelemetryDataFragment; + type FileGetClassification = { + owner: 'bpasero'; + } & FileTelemetryDataFragment; this.telemetryService.publicLog2<TelemetryData, FileGetClassification>('fileGet', this.getTelemetryData(e.model.resource, e.reason)); } @@ -149,11 +155,14 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr const settingsType = this.getTypeIfSettings(e.model.resource); if (settingsType) { type SettingsWrittenClassification = { + owner: 'bpasero'; settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data } else { - type FilePutClassfication = {} & FileTelemetryDataFragment; + type FilePutClassfication = { + owner: 'bpasero'; + } & FileTelemetryDataFragment; this.telemetryService.publicLog2<TelemetryData, FilePutClassfication>('filePUT', this.getTelemetryData(e.model.resource, e.reason)); } } @@ -164,17 +173,17 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr } // Check for global settings file - if (isEqual(resource, this.environmentService.settingsResource)) { + if (isEqual(resource, this.userDataProfileService.currentProfile.settingsResource)) { return 'global-settings'; } // Check for keybindings file - if (isEqual(resource, this.environmentService.keybindingsResource)) { + if (isEqual(resource, this.userDataProfileService.currentProfile.keybindingsResource)) { return 'keybindings'; } // Check for snippets - if (isEqualOrParent(resource, this.environmentService.snippetsHome)) { + if (isEqualOrParent(resource, this.userDataProfileService.currentProfile.snippetsHome)) { return 'snippets'; } diff --git a/src/vs/workbench/contrib/terminal/browser/links/links.ts b/src/vs/workbench/contrib/terminal/browser/links/links.ts index 012b95f3214..ad7f84b6141 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/links.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/links.ts @@ -18,6 +18,12 @@ export interface ITerminalLinkDetector { readonly xterm: Terminal; /** + * The maximum link length possible for this detector, this puts a cap on how much of a wrapped + * line to consider to prevent performance problems. + */ + readonly maxLinkLength: number; + + /** * Detects links within the _wrapped_ line range provided and returns them as an array. * * @param lines The individual buffer lines that make up the wrapped line. @@ -73,30 +79,30 @@ export const enum TerminalBuiltinLinkType { /** * The link is validated to be a file on the file system and will open an editor. */ - LocalFile, + LocalFile = 'LocalFile', /** * The link is validated to be a folder on the file system and is outside the workspace. It will * reveal the folder within the explorer. */ - LocalFolderOutsideWorkspace, + LocalFolderOutsideWorkspace = 'LocalFolderOutsideWorkspace', /** * The link is validated to be a folder on the file system and is within the workspace and will * reveal the folder within the explorer. */ - LocalFolderInWorkspace, + LocalFolderInWorkspace = 'LocalFolderInWorkspace', /** * A low confidence link which will search for the file in the workspace. If there is a single * match, it will open the file; otherwise, it will present the matches in a quick pick. */ - Search, + Search = 'Search', /** * A link whose text is a valid URI. */ - Url + Url = 'Url' } export interface ITerminalExternalLinkType { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts index 9a633e8956a..521a7b2ee44 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts @@ -8,14 +8,9 @@ import { convertLinkRangeToBuffer, getXtermLineContent } from 'vs/workbench/cont import { ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IBufferLine, Terminal } from 'xterm'; -const enum Constants { - /** - * The max line length to try extract word links from. - */ - MaxLineLength = 2000 -} - export class TerminalExternalLinkDetector implements ITerminalLinkDetector { + readonly maxLinkLength = 2000; + constructor( readonly id: string, readonly xterm: Terminal, @@ -26,7 +21,7 @@ export class TerminalExternalLinkDetector implements ITerminalLinkDetector { async detect(lines: IBufferLine[], startLine: number, endLine: number): Promise<ITerminalSimpleLink[]> { // Get the text representation of the wrapped line const text = getXtermLineContent(this.xterm.buffer.active, startLine, endLine, this.xterm.cols); - if (text === '' || text.length > Constants.MaxLineLength) { + if (text === '' || text.length > this.maxLinkLength) { return []; } diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts index 868bf9600e8..43d8923d291 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts @@ -59,12 +59,19 @@ export class TerminalLinkDetectorAdapter extends Disposable implements ILinkProv this._detector.xterm.buffer.active.getLine(startLine)! ]; - while (startLine >= 0 && this._detector.xterm.buffer.active.getLine(startLine)?.isWrapped) { + // Cap the maximum context on either side of the line being provided, by taking the context + // around the line being provided for this ensures the line the pointer is on will have + // links provided. + const maxLineContext = Math.max(this._detector.maxLinkLength / this._detector.xterm.cols); + const minStartLine = Math.max(startLine - maxLineContext, 0); + const maxEndLine = Math.min(endLine + maxLineContext, this._detector.xterm.buffer.active.length); + + while (startLine >= minStartLine && this._detector.xterm.buffer.active.getLine(startLine)?.isWrapped) { lines.unshift(this._detector.xterm.buffer.active.getLine(startLine - 1)!); startLine--; } - while (endLine < this._detector.xterm.buffer.active.length && this._detector.xterm.buffer.active.getLine(endLine + 1)?.isWrapped) { + while (endLine < maxEndLine && this._detector.xterm.buffer.active.getLine(endLine + 1)?.isWrapped) { lines.push(this._detector.xterm.buffer.active.getLine(endLine + 1)!); endLine++; } diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts index 202ba173cc0..bb6042ce80a 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts @@ -122,6 +122,10 @@ export function convertBufferRangeToViewport(bufferRange: IBufferRange, viewport } export function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string { + // Cap the maximum number of lines generated to prevent potential performance problems. This is + // more of a sanity check as the wrapped line should already be trimmed down at this point. + const maxLineLength = Math.max(2048 / cols * 2); + lineEnd = Math.min(lineEnd, lineStart + maxLineLength); let content = ''; for (let i = lineStart; i <= lineEnd; i++) { // Make sure only 0 to cols are considered as resizing when windows mode is enabled will diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts index d56a7fe620e..28d0c22794f 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts @@ -57,6 +57,8 @@ export class TerminalLinkManager extends DisposableStore { // both local and remote terminals are present private readonly _resolvedLinkCache = new LinkCache(); + private _lastTopLine: number | undefined; + constructor( private readonly _xterm: Terminal, private readonly _processManager: ITerminalProcessManager, @@ -86,7 +88,7 @@ export class TerminalLinkManager extends DisposableStore { this._openers.set(TerminalBuiltinLinkType.LocalFile, localFileOpener); this._openers.set(TerminalBuiltinLinkType.LocalFolderInWorkspace, localFolderInWorkspaceOpener); this._openers.set(TerminalBuiltinLinkType.LocalFolderOutsideWorkspace, this._instantiationService.createInstance(TerminalLocalFolderOutsideWorkspaceLinkOpener)); - this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, localFolderInWorkspaceOpener, this._processManager.os || OS)); + this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, this._processManager.getInitialCwd(), localFileOpener, localFolderInWorkspaceOpener, this._processManager.os || OS)); this._openers.set(TerminalBuiltinLinkType.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processManager.remoteAuthority)); this._registerStandardLinkProviders(); @@ -141,12 +143,20 @@ export class TerminalLinkManager extends DisposableStore { return links[0]; } - async getLinks(): Promise<IDetectedLinks> { + async getLinks(extended?: boolean): Promise<IDetectedLinks> { const wordResults: ILink[] = []; const webResults: ILink[] = []; const fileResults: ILink[] = []; - - for (let i = this._xterm.buffer.active.length - 1; i >= this._xterm.buffer.active.viewportY; i--) { + let noMoreResults: boolean = false; + let topLine = !extended ? this._xterm.buffer.active.viewportY - Math.min(this._xterm.rows, 50) : this._lastTopLine! - 1000; + if (topLine < 0 || topLine - Math.min(this._xterm.rows, 50) < 0) { + noMoreResults = true; + } + if (topLine < 0) { + topLine = 0; + } + this._lastTopLine = topLine; + for (let i = this._xterm.buffer.active.length - 1; i >= topLine; i--) { const links = await this._getLinksForLine(i); if (links) { const { wordLinks, webLinks, fileLinks } = links; @@ -161,11 +171,11 @@ export class TerminalLinkManager extends DisposableStore { } } } - return { webLinks: webResults, fileLinks: fileResults, wordLinks: wordResults }; + return { webLinks: webResults, fileLinks: fileResults, wordLinks: wordResults, noMoreResults }; } private async _getLinksForLine(y: number): Promise<IDetectedLinks | undefined> { - let unfilteredWordLinks = await this._getLinksForType(y, 'word'); + const unfilteredWordLinks = await this._getLinksForType(y, 'word'); const webLinks = await this._getLinksForType(y, 'url'); const fileLinks = await this._getLinksForType(y, 'localFile'); const words = new Set(); @@ -469,6 +479,7 @@ export interface IDetectedLinks { wordLinks?: ILink[]; webLinks?: ILink[]; fileLinks?: ILink[]; + noMoreResults?: boolean; } const enum LinkCacheConstants { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts index 043a8472b5e..bfc044ba133 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts @@ -52,12 +52,27 @@ export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { * @param link Url link which may contain line and column number. */ extractLineColumnInfo(link: string): ILineColumnInfo { - const matches: string[] | null = getLocalLinkRegex(this._os).exec(link); const lineColumnInfo: ILineColumnInfo = { lineNumber: 1, columnNumber: 1 }; + // The local link regex only works for non file:// links, check these for a simple + // `:line:col` suffix + if (link.startsWith('file://')) { + const simpleMatches = link.match(/:(\d+)(:(\d+))?$/); + if (simpleMatches) { + if (simpleMatches[1] !== undefined) { + lineColumnInfo.lineNumber = parseInt(simpleMatches[1]); + } + if (simpleMatches[3] !== undefined) { + lineColumnInfo.columnNumber = parseInt(simpleMatches[3]); + } + } + return lineColumnInfo; + } + + const matches: string[] | null = getLocalLinkRegex(this._os).exec(link); if (!matches) { return lineColumnInfo; } @@ -110,6 +125,7 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { constructor( private readonly _capabilities: ITerminalCapabilityStore, + private readonly _initialCwd: Promise<string>, private readonly _localFileOpener: TerminalLocalFileLinkOpener, private readonly _localFolderInWorkspaceOpener: TerminalLocalFolderInWorkspaceLinkOpener, private readonly _os: OperatingSystem, @@ -138,39 +154,44 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { return; } }); - let matchLink = text; + let cwdResolvedText = text; if (this._capabilities.has(TerminalCapability.CommandDetection)) { - matchLink = updateLinkWithRelativeCwd(this._capabilities, link.bufferRange.start.y, text, pathSeparator) || text; + cwdResolvedText = updateLinkWithRelativeCwd(this._capabilities, link.bufferRange.start.y, text, pathSeparator) || text; } - const sanitizedLink = matchLink.replace(/:\d+(:\d+)?$/, ''); - try { - const result = await this._getExactMatch(sanitizedLink); - if (result) { - const { uri, isDirectory } = result; - const linkToOpen = { - // 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 - }; - if (uri) { - return isDirectory ? this._localFolderInWorkspaceOpener.open(linkToOpen) : this._localFileOpener.open(linkToOpen); - } + + // Try open the cwd resolved link first + if (await this._tryOpenExactLink(cwdResolvedText, link)) { + return; + } + + // If the cwd resolved text didn't match, try find the link without the cwd resolved, for + // example when a command prints paths in a sub-directory of the current cwd + if (text !== cwdResolvedText) { + if (await this._tryOpenExactLink(text, link)) { + return; } - } catch { - // Fallback to searching quick access - return this._quickInputService.quickAccess.show(text); } + // Fallback to searching quick access return this._quickInputService.quickAccess.show(text); } private async _getExactMatch(sanitizedLink: string): Promise<IResourceMatch | undefined> { + // Make the link relative to the cwd if it isn't absolute + const pathModule = osPathModule(this._os); + const isAbsolute = pathModule.isAbsolute(sanitizedLink); + let absolutePath: string | undefined = isAbsolute ? sanitizedLink : undefined; + const initialCwd = await this._initialCwd; + if (!isAbsolute && initialCwd.length > 0) { + absolutePath = pathModule.join(initialCwd, sanitizedLink); + } + + // Try open as an absolute link let resourceMatch: IResourceMatch | undefined; - if (osPathModule(this._os).isAbsolute(sanitizedLink)) { + if (absolutePath) { + const slashNormalizedPath = this._os === OperatingSystem.Windows ? absolutePath.replace(/\\/g, '/') : absolutePath; const scheme = this._workbenchEnvironmentService.remoteAuthority ? Schemas.vscodeRemote : Schemas.file; - const uri = URI.from({ scheme, path: sanitizedLink }); + const uri = URI.from({ scheme, path: slashNormalizedPath }); try { const fileStat = await this._fileService.stat(uri); resourceMatch = { uri, isDirectory: fileStat.isDirectory }; @@ -178,6 +199,8 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { // File or dir doesn't exist, continue on } } + + // Search the workspace if an exact match based on the absolute path was not found if (!resourceMatch) { const results = await this._searchService.fileSearch( this._fileQueryBuilder.file(this._workspaceContextService.getWorkspace().folders, { @@ -192,6 +215,30 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { } return resourceMatch; } + + private async _tryOpenExactLink(text: string, link: ITerminalSimpleLink): Promise<boolean> { + const sanitizedLink = text.replace(/:\d+(:\d+)?$/, ''); + try { + const result = await this._getExactMatch(sanitizedLink); + if (result) { + const { uri, isDirectory } = result; + const linkToOpen = { + // Use the absolute URI's path here so the optional line/col get detected + text: result.uri.fsPath + (text.match(/:\d+(:\d+)?$/)?.[0] || ''), + uri, + bufferRange: link.bufferRange, + type: link.type + }; + if (uri) { + await (isDirectory ? this._localFolderInWorkspaceOpener.open(linkToOpen) : this._localFileOpener.open(linkToOpen)); + return true; + } + } + } catch { + return false; + } + return false; + } } interface IResourceMatch { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick.ts index 1a2b18727b5..3a44a394e62 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { EventType } from 'vs/base/browser/dom'; +import { Emitter } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IDetectedLinks } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; @@ -11,6 +12,9 @@ import { TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browse import { ILink } from 'xterm'; export class TerminalLinkQuickpick { + + private readonly _onDidRequestMoreLinks = new Emitter<void>(); + readonly onDidRequestMoreLinks = this._onDidRequestMoreLinks.event; constructor( @IQuickInputService private readonly _quickInputService: IQuickInputService ) { } @@ -21,7 +25,8 @@ export class TerminalLinkQuickpick { const webPicks = links.webLinks ? await this._generatePicks(links.webLinks) : undefined; const options = { placeHolder: localize('terminal.integrated.openDetectedLink', "Select the link to open"), - canPickMany: false + canPickMany: false, + }; const picks: LinkQuickPickItem[] = []; if (webPicks) { @@ -36,13 +41,21 @@ export class TerminalLinkQuickpick { picks.push({ type: 'separator', label: localize('terminal.integrated.searchLinks', "Workspace Search") }); picks.push(...wordPicks); } - + picks.push({ type: 'separator' }); + if (!links.noMoreResults) { + const showMoreItem = { label: localize('terminal.integrated.showMoreLinks', "Show more links") }; + picks.push(showMoreItem); + } const pick = await this._quickInputService.pick(picks, options); if (!pick) { return; } const event = new TerminalLinkQuickPickEvent(EventType.CLICK); - pick.link.activate(event, pick.label); + if ('link' in pick) { + pick.link.activate(event, pick.label); + } else { + this._onDidRequestMoreLinks.fire(); + } return; } @@ -67,4 +80,4 @@ export interface ITerminalLinkQuickPickItem extends IQuickPickItem { link: ILink; } -type LinkQuickPickItem = ITerminalLinkQuickPickItem | IQuickPickSeparator; +type LinkQuickPickItem = ITerminalLinkQuickPickItem | IQuickPickSeparator | IQuickPickItem; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts index 936af397072..48e5c0659bc 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts @@ -67,6 +67,12 @@ export const lineAndColumnClauseGroupCount = 6; export class TerminalLocalLinkDetector implements ITerminalLinkDetector { static id = 'local'; + // This was chosen as a reasonable maximum line length given the tradeoff between performance + // and how likely it is to encounter such a large line length. Some useful reference points: + // - Window old max length: 260 ($MAX_PATH) + // - Linux max length: 4096 ($PATH_MAX) + readonly maxLinkLength = 500; + constructor( readonly xterm: Terminal, private readonly _capabilities: ITerminalCapabilityStore, diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts index 948b257d8ab..ffe6418a869 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts @@ -17,18 +17,15 @@ const enum Constants { * The maximum number of links in a line to resolve against the file system. This limit is put * in place to avoid sending excessive data when remote connections are in place. */ - MaxResolvedLinksInLine = 10, - - /** - * The maximum length of a link to resolve against the file system. This limit is put in place - * to avoid sending excessive data when remote connections are in place. - */ - MaxResolvedLinkLength = 1024, + MaxResolvedLinksInLine = 10 } export class TerminalUriLinkDetector implements ITerminalLinkDetector { static id = 'uri'; + // 2048 is the maximum URL length + readonly maxLinkLength = 2048; + constructor( readonly xterm: Terminal, private readonly _resolvePath: (link: string, uri?: URI) => Promise<ResolvedLink>, @@ -49,7 +46,7 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector { // Check if the link is within the mouse position const uri = computedLink.url - ? (typeof computedLink.url === 'string' ? URI.parse(computedLink.url) : computedLink.url) + ? (typeof computedLink.url === 'string' ? URI.parse(this._excludeLineAndColSuffix(computedLink.url)) : computedLink.url) : undefined; if (!uri) { @@ -58,6 +55,11 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector { const text = computedLink.url?.toString() || ''; + // Don't try resolve any links of excessive length + if (text.length > this.maxLinkLength) { + continue; + } + // Handle non-file scheme links if (uri.scheme !== Schemas.file) { links.push({ @@ -69,11 +71,6 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector { continue; } - // Don't try resolve any links of excessive length - if (text.length > Constants.MaxResolvedLinkLength) { - continue; - } - // Filter out URI with unrecognized authorities if (uri.authority.length !== 2 && uri.authority.endsWith(':')) { continue; @@ -94,7 +91,8 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector { type = TerminalBuiltinLinkType.LocalFile; } links.push({ - text: linkStat.link, + // Use computedLink.url if it's a string to retain the line/col suffix + text: typeof computedLink.url === 'string' ? computedLink.url : linkStat.link, uri: linkStat.uri, bufferRange, type @@ -119,6 +117,10 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector { } return false; } + + private _excludeLineAndColSuffix(path: string): string { + return path.replace(/:\d+(:\d+)?$/, ''); + } } class TerminalLinkAdapter implements ILinkComputerTarget { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts index 6bfa63c5038..637ff8dad9c 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts @@ -25,6 +25,10 @@ interface Word { export class TerminalWordLinkDetector implements ITerminalLinkDetector { static id = 'word'; + // Word links typically search the workspace so it makes sense that their maximum link length is + // quite small. + readonly maxLinkLength = 100; + constructor( readonly xterm: Terminal, @IConfigurationService private readonly _configurationService: IConfigurationService, 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 ec96e7d3fd3..b0c525a298d 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -3,29 +3,46 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- +# Prevent the script recursing when setting up +if [[ -n "$VSCODE_SHELL_INTEGRATION" ]]; then + builtin return +fi + VSCODE_SHELL_INTEGRATION=1 -if [ -z "$VSCODE_SHELL_LOGIN" ]; then - . ~/.bashrc -else - # Imitate -l because --init-file doesn't support it: - # run the first of these files that exists - if [ -f /etc/profile ]; then - . /etc/profile - fi - # exceute the first that exists - if [ -f ~/.bash_profile ]; then - . ~/.bash_profile - elif [ -f ~/.bash_login ]; then - . ~/.bash_login - elif [ -f ~/.profile ]; then - . ~/.profile +# Run relevant rc/profile only if shell integration has been injected, not when run manually +if [ "$VSCODE_INJECTION" == "1" ]; then + if [ -z "$VSCODE_SHELL_LOGIN" ]; then + . ~/.bashrc + else + # Imitate -l because --init-file doesn't support it: + # run the first of these files that exists + if [ -f /etc/profile ]; then + . /etc/profile + fi + # exceute the first that exists + if [ -f ~/.bash_profile ]; then + . ~/.bash_profile + elif [ -f ~/.bash_login ]; then + . ~/.bash_login + elif [ -f ~/.profile ]; then + . ~/.profile + fi + builtin unset VSCODE_SHELL_LOGIN fi - VSCODE_SHELL_LOGIN="" + builtin unset VSCODE_INJECTION fi +# Disable shell integration if PROMPT_COMMAND is 2+ function calls since that is not handled. if [[ "$PROMPT_COMMAND" =~ .*(' '.*\;)|(\;.*' ').* ]]; then - VSCODE_SHELL_INTEGRATION="" + builtin unset VSCODE_SHELL_INTEGRATION + builtin return +fi + +# Disable shell integration if HISTCONTROL is set to erase duplicate entries as the exit code +# reporting relies on the duplicates existing +if [[ "$HISTCONTROL" =~ .*erasedups.* ]]; then + builtin unset VSCODE_SHELL_INTEGRATION builtin return fi @@ -33,6 +50,11 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi +__vsc_initialized=0 +__vsc_original_PS1="$PS1" +__vsc_original_PS2="$PS2" +__vsc_custom_PS1="" +__vsc_custom_PS2="" __vsc_in_command_execution="1" __vsc_last_history_id=$(history 1 | awk '{print $1;}') @@ -70,47 +92,70 @@ __vsc_command_complete() { fi __vsc_update_cwd } - __vsc_update_prompt() { - __vsc_prior_prompt="$PS1" - __vsc_in_command_execution="" - PS1="\[$(__vsc_prompt_start)\]$PREFIX$PS1\[$(__vsc_prompt_end)\]" - PS2="\[$(__vsc_continuation_start)\]$PS2\[$(__vsc_continuation_end)\]" + # in command execution + if [ "$__vsc_in_command_execution" = "1" ]; then + # Wrap the prompt if it is not yet wrapped, if the PS1 changed this this was last set it + # means the user re-exported the PS1 so we should re-wrap it + if [[ "$__vsc_custom_PS1" == "" || "$__vsc_custom_PS1" != "$PS1" ]]; then + __vsc_original_PS1=$PS1 + __vsc_custom_PS1="\[$(__vsc_prompt_start)\]$PREFIX$__vsc_original_PS1\[$(__vsc_prompt_end)\]" + PS1="$__vsc_custom_PS1" + fi + if [[ "$__vsc_custom_PS2" == "" || "$__vsc_custom_PS2" != "$PS2" ]]; then + __vsc_original_PS2=$PS2 + __vsc_custom_PS2="\[$(__vsc_continuation_start)\]$__vsc_original_PS2\[$(__vsc_continuation_end)\]" + PS2="$__vsc_custom_PS2" + fi + __vsc_in_command_execution="0" + fi } __vsc_precmd() { __vsc_command_complete "$__vsc_status" - - # in command execution - if [ -n "$__vsc_in_command_execution" ]; then - # non null - __vsc_update_prompt - fi + __vsc_update_prompt } -# capture any debug trap so it is not overwritten -__vsc_original_trap="$(trap -p DEBUG)" -if [[ -n "$__vsc_original_trap" ]]; then - __vsc_original_trap=${__vsc_original_trap#'trap -- '*} - __vsc_original_trap=${__vsc_original_trap%'DEBUG'} -fi - __vsc_preexec() { - eval ${__vsc_original_trap} - PS1="$__vsc_prior_prompt" - if [ -z "${__vsc_in_command_execution-}" ]; then + if [ "$__vsc_in_command_execution" = "0" ]; then + __vsc_initialized=1 __vsc_in_command_execution="1" __vsc_command_output_start fi } +# Debug trapping/preexec inspired by starship (ISC) +if [[ -n "${bash_preexec_imported:-}" ]]; then + __vsc_preexec_only() { + __vsc_status="$?" + __vsc_preexec + } + precmd_functions+=(__vsc_prompt_cmd) + preexec_functions+=(__vsc_preexec_only) +else + __vsc_dbg_trap="$(trap -p DEBUG | cut -d' ' -f3 | tr -d \')" + if [[ -z "$__vsc_dbg_trap" ]]; then + __vsc_preexec_only() { + __vsc_status="$?" + __vsc_preexec + } + trap '__vsc_preexec_only "$_"' DEBUG + elif [[ "$__vsc_dbg_trap" != '__vsc_preexec "$_"' && "$__vsc_dbg_trap" != '__vsc_preexec_all "$_"' ]]; then + __vsc_preexec_all() { + __vsc_status="$?" + builtin eval ${__vsc_dbg_trap} + __vsc_preexec + } + trap '__vsc_preexec_all "$_"' DEBUG + fi +fi + __vsc_update_prompt __vsc_prompt_cmd_original() { if [[ ${IFS+set} ]]; then __vsc_original_ifs="$IFS" fi - __vsc_status="$?" if [[ "$__vsc_original_prompt_command" =~ .+\;.+ ]]; then IFS=';' else @@ -124,7 +169,7 @@ __vsc_prompt_cmd_original() { unset IFS fi for ((i = 0; i < ${#ADDR[@]}; i++)); do - # unset IFS + (exit ${__vsc_status}) builtin eval ${ADDR[i]} done __vsc_precmd @@ -143,10 +188,10 @@ else __vsc_original_prompt_command=${PROMPT_COMMAND[@]} fi -if [[ -n "$__vsc_original_prompt_command" && "$__vsc_original_prompt_command" != "__vsc_prompt_cmd" ]]; then - PROMPT_COMMAND=__vsc_prompt_cmd_original -else - PROMPT_COMMAND=__vsc_prompt_cmd +if [[ -z "${bash_preexec_imported:-}" ]]; then + if [[ -n "$__vsc_original_prompt_command" && "$__vsc_original_prompt_command" != "__vsc_prompt_cmd" ]]; then + PROMPT_COMMAND=__vsc_prompt_cmd_original + else + PROMPT_COMMAND=__vsc_prompt_cmd + fi fi - -trap '__vsc_preexec' DEBUG diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh index e1f6a34d675..a94e7c11c71 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh @@ -4,15 +4,23 @@ # --------------------------------------------------------------------------------------------- builtin autoload -Uz add-zsh-hook +# Prevent the script recursing when setting up +if [ -n "$VSCODE_SHELL_INTEGRATION" ]; then + builtin return +fi + # This variable allows the shell to both detect that VS Code's shell integration is enabled as well # as disable it by unsetting the variable. VSCODE_SHELL_INTEGRATION=1 -if [[ $options[norcs] = off && -f $USER_ZDOTDIR/.zshrc ]]; then - VSCODE_ZDOTDIR=$ZDOTDIR - ZDOTDIR=$USER_ZDOTDIR - . $USER_ZDOTDIR/.zshrc - ZDOTDIR=$VSCODE_ZDOTDIR +# Only fix up ZDOTDIR if shell integration was injected (not manually installed) and has not been called yet +if [[ "$VSCODE_INJECTION" == "1" ]]; then + if [[ $options[norcs] = off && -f $USER_ZDOTDIR/.zshrc ]]; then + VSCODE_ZDOTDIR=$ZDOTDIR + ZDOTDIR=$USER_ZDOTDIR + . $USER_ZDOTDIR/.zshrc + ZDOTDIR=$VSCODE_ZDOTDIR + fi fi # Shell integration was disabled by the shell, exit without warning assuming either the shell has @@ -21,6 +29,7 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi +__vsc_initialized="0" __vsc_in_command_execution="1" __vsc_last_history_id=0 @@ -58,15 +67,28 @@ __vsc_right_prompt_end() { __vsc_command_complete() { builtin local __vsc_history_id=$(builtin history | tail -n1 | awk '{print $1;}') - if [[ "$__vsc_history_id" == "$__vsc_last_history_id" ]]; then - builtin printf "\033]633;D\007" + # Don't write the command complete sequence for the first prompt without an associated command + if [[ "$__vsc_initialized" == "1" ]]; then + if [[ "$__vsc_history_id" == "$__vsc_last_history_id" ]]; then + builtin printf "\033]633;D\007" + else + builtin printf "\033]633;D;%s\007" "$__vsc_status" + __vsc_last_history_id=$__vsc_history_id + fi else - builtin printf "\033]633;D;%s\007" "$__vsc_status" - __vsc_last_history_id=$__vsc_history_id + builtin printf "\033]633;D\007" fi __vsc_update_cwd } +if [[ -o NOUNSET ]]; then + if [ -z "${RPROMPT-}" ]; then + RPROMPT="" + fi + if [ -z "${PREFIX-}" ]; then + PREFIX="" + fi +fi __vsc_update_prompt() { __vsc_prior_prompt="$PS1" __vsc_in_command_execution="" @@ -99,6 +121,7 @@ __vsc_preexec() { if [ -n "$RPROMPT" ]; then RPROMPT="$__vsc_prior_rprompt" fi + __vsc_initialized="1" __vsc_in_command_execution="1" __vsc_command_output_start } diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index 83b347b25d8..aceb31ab780 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -3,6 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- +# Prevent installing more than once per session +if (Test-Path variable:global:__VSCodeOriginalPrompt) { + return; +} + $Global:__VSCodeOriginalPrompt = $function:Prompt $Global:__LastHistoryId = -1 diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 945161cad7f..17933a5776d 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -39,7 +39,7 @@ .monaco-workbench .editor-instance .terminal-wrapper, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper { - display: none; + display: block; height: 100%; box-sizing: border-box; } @@ -87,13 +87,12 @@ .monaco-workbench .simple-find-part-wrapper.result-count { z-index: 33 !important; - padding-right: 80px; } .result-count .simple-find-part { - width: 280px; - max-width: 280px; - min-width: 280px; + width: 330px; + max-width: 330px; + min-width: 330px; } .result-count .matchesCount { @@ -113,11 +112,6 @@ .xterm.xterm-cursor-pointer .xterm-screen { cursor: pointer; } .xterm.column-select.focus .xterm-screen { cursor: crosshair; } -.monaco-workbench .editor-instance .terminal-wrapper.active, -.monaco-workbench .pane-body.integrated-terminal .terminal-wrapper.active { - display: block; -} - .monaco-workbench .editor-instance .xterm { padding-left: 20px !important; } @@ -459,8 +453,14 @@ outline-style: dotted !important; } -.hc-black .xterm-find-active-result-decoration, -.hc-light .xterm-find-active-result-decoration { +.hc-black .xterm-find-result-decoration, +.hc-light .xterm-find-result-decoration { + outline-style: solid !important; +} + +.xterm-find-active-result-decoration { outline-style: solid !important; outline-width: 2px !important; + /* Ensure the active decoration is above the regular decoration */ + z-index: 7 !important; } diff --git a/src/vs/workbench/contrib/terminal/browser/media/xterm.css b/src/vs/workbench/contrib/terminal/browser/media/xterm.css index 30ac0800d21..a7292928fbb 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/xterm.css +++ b/src/vs/workbench/contrib/terminal/browser/media/xterm.css @@ -40,6 +40,7 @@ */ .xterm { + cursor: text; position: relative; user-select: none; -ms-user-select: none; @@ -128,10 +129,6 @@ line-height: normal; } -.xterm { - cursor: text; -} - .xterm.enable-mouse-events { /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ cursor: default; @@ -188,4 +185,10 @@ position: absolute; top: 0; right: 0; + pointer-events: none; +} + +.xterm-decoration-top { + z-index: 2; + position: relative; } diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index ab808bab234..90bbde931bd 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -38,7 +38,8 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { hasChildProcesses: true, resolvedShellLaunchConfig: {}, overrideDimensions: undefined, - failedShellIntegrationActivation: false + failedShellIntegrationActivation: false, + usedShellIntegrationInjection: undefined }; get id(): number { return this._id; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 104e9f455e1..d01b958ae13 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -68,7 +68,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: TerminalQuickAccessProvider.PREFIX, contextKey: inTerminalsPicker, placeholder: nls.localize('tasksQuickAccessPlaceholder', "Type the name of a terminal to open."), - helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Show All Opened Terminals"), needsEditor: false }] + helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Show All Opened Terminals"), commandId: TerminalCommandId.QuickOpenTerm }] }); const quickAccessNavigateNextInTerminalPickerId = 'workbench.action.quickOpenNavigateNextInTerminalPicker'; CommandsRegistry.registerCommand({ id: quickAccessNavigateNextInTerminalPickerId, handler: getQuickNavigateHandler(quickAccessNavigateNextInTerminalPickerId, true) }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 6d1494b10d3..ddaf6f211db 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -3,21 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ProcessPropertyType, IProcessPropertyMap, IShellIntegration } from 'vs/platform/terminal/common/terminal'; -import { INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalBackend, ITerminalProcessExtHostProxy, IRegisterContributedProfileArgs } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; -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, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { IExtensionTerminalProfile, IProcessPropertyMap, IShellIntegration, IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, ProcessPropertyType, TerminalIcon, TerminalLocation, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IGenericMarkProperties } from 'vs/platform/terminal/common/terminalProcess'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IEditableData } from 'vs/workbench/common/views'; +import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; +import { INavigationMode, IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalBackend, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; +import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; +import { IMarker } from 'xterm'; export const ITerminalService = createDecorator<ITerminalService>('terminalService'); export const ITerminalEditorService = createDecorator<ITerminalEditorService>('terminalEditorService'); @@ -339,6 +341,7 @@ export interface ITerminalGroupService extends ITerminalInstanceHost, ITerminalF hidePanel(): void; focusTabs(): void; showTabs(): void; + updateVisibility(): void; } /** @@ -459,6 +462,11 @@ export interface ITerminalInstance { target?: TerminalLocation; /** + * Whether or not shell integration telemetry / warnings should be reported for this terminal. + */ + disableShellIntegrationReporting: boolean; + + /** * The id of a persistent process. This is defined if this is a terminal created by a pty host * that supports reconnection. */ @@ -630,6 +638,17 @@ export interface ITerminalInstance { showEnvironmentInfoHover(): void; /** + * Registers and returns a marker + */ + registerMarker(): IMarker | undefined; + + /** + * Adds a decoration to the buffer at the @param marker with + * @param genericMarkProperties + */ + addGenericMark(marker: IMarker, genericMarkProperties: IGenericMarkProperties): void; + + /** * Dispose the terminal instance, removing it from the panel/service and freeing up resources. * * @param immediate Whether the kill should be immediate or not. Immediate should only be used @@ -665,6 +684,12 @@ export interface ITerminalInstance { clearSelection(): void; /** + * When the panel is hidden or a terminal in the editor area becomes inactive, reset the focus context key + * to avoid issues like #147180. + */ + resetFocusContextKey(): void; + + /** * Select all text in the terminal. */ selectAll(): void; @@ -741,7 +766,7 @@ export interface ITerminalInstance { * * @param container The element to attach the terminal instance to. */ - attachToElement(container: HTMLElement): Promise<void> | void; + attachToElement(container: HTMLElement): void; /** * Detaches the terminal instance from the terminal editor DOM element. @@ -913,6 +938,22 @@ export interface IXtermTerminal { * Clears the active search result decorations */ clearActiveSearchDecoration(): void; + + /** + * Adds a decoration at the @param marker with the given properties + * @param properties + */ + addDecoration(marker: IMarker, properties: IGenericMarkProperties): void; +} + +export interface IInternalXtermTerminal { + /** + * Writes text directly to the terminal, bypassing the process. + * + * **WARNING:** This should never be used outside of the terminal component and only for + * developer purposed inside the terminal component. + */ + _writeText(data: string): void; // eslint-disable-line @typescript-eslint/naming-convention } export interface IRequestAddInstanceToGroupEvent { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 90d402ce3ed..ed78d4a79e8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -34,7 +34,7 @@ import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/w import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; -import { Direction, ICreateTerminalOptions, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { Direction, ICreateTerminalOptions, IInternalXtermTerminal, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, ITerminalProfileService, TerminalCommandId, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -51,7 +51,8 @@ import { AbstractVariableResolverService } from 'vs/workbench/services/configura import { ITerminalQuickPickItem } from 'vs/workbench/contrib/terminal/browser/terminalProfileQuickpick'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { getIconId, getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; -import { getCommandHistory } from 'vs/workbench/contrib/terminal/common/history'; +import { clearShellFileHistory, getCommandHistory } from 'vs/workbench/contrib/terminal/common/history'; +import { CATEGORIES } from 'vs/workbench/common/actions'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -733,14 +734,22 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.navigationModeFocusPrevious', "Focus Previous Line (Navigation Mode)"), original: 'Focus Previous Line (Navigation Mode)' }, f1: true, category, - keybinding: { + keybinding: [{ + primary: KeyCode.UpArrow, + when: ContextKeyExpr.or( + ContextKeyExpr.and(TerminalContextKeys.a11yTreeFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.navigationModeActive), + ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.navigationModeActive), + ), + weight: KeybindingWeight.WorkbenchContrib + }, + { primary: KeyMod.CtrlCmd | KeyCode.UpArrow, when: ContextKeyExpr.or( ContextKeyExpr.and(TerminalContextKeys.a11yTreeFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED), ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED) ), weight: KeybindingWeight.WorkbenchContrib - }, + }], precondition: TerminalContextKeys.processSupported }); } @@ -751,18 +760,48 @@ export function registerTerminalActions() { registerAction2(class extends Action2 { constructor() { super({ + id: TerminalCommandId.NavigationModeFocusPreviousPage, + title: { value: localize('workbench.action.terminal.navigationModeFocusPreviousPage', "Focus Previous Page (Navigation Mode)"), original: 'Focus Previous Page (Navigation Mode)' }, + f1: true, + category, + keybinding: [{ + primary: KeyCode.PageUp, + when: ContextKeyExpr.or( + ContextKeyExpr.and(TerminalContextKeys.a11yTreeFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.navigationModeActive), + ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.navigationModeActive), + ), + weight: KeybindingWeight.WorkbenchContrib + }], + precondition: TerminalContextKeys.processSupported + }); + } + run(accessor: ServicesAccessor) { + accessor.get(ITerminalService).activeInstance?.navigationMode?.focusPreviousPage(); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ id: TerminalCommandId.NavigationModeFocusNext, title: { value: localize('workbench.action.terminal.navigationModeFocusNext', "Focus Next Line (Navigation Mode)"), original: 'Focus Next Line (Navigation Mode)' }, f1: true, category, - keybinding: { + keybinding: [{ + primary: KeyCode.DownArrow, + when: ContextKeyExpr.or( + ContextKeyExpr.and(TerminalContextKeys.a11yTreeFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.navigationModeActive), + ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.navigationModeActive), + ), + weight: KeybindingWeight.WorkbenchContrib + }, + { primary: KeyMod.CtrlCmd | KeyCode.DownArrow, when: ContextKeyExpr.or( ContextKeyExpr.and(TerminalContextKeys.a11yTreeFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED), ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED) ), weight: KeybindingWeight.WorkbenchContrib - }, + }], precondition: TerminalContextKeys.processSupported }); } @@ -773,6 +812,28 @@ export function registerTerminalActions() { registerAction2(class extends Action2 { constructor() { super({ + id: TerminalCommandId.NavigationModeFocusNextPage, + title: { value: localize('workbench.action.terminal.navigationModeFocusNextPage', "Focus Next Page (Navigation Mode)"), original: 'Focus Next Page (Navigation Mode)' }, + f1: true, + category, + keybinding: [{ + primary: KeyCode.PageDown, + when: ContextKeyExpr.or( + ContextKeyExpr.and(TerminalContextKeys.a11yTreeFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.navigationModeActive), + ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED, TerminalContextKeys.navigationModeActive), + ), + weight: KeybindingWeight.WorkbenchContrib + }], + precondition: TerminalContextKeys.processSupported + }); + } + run(accessor: ServicesAccessor) { + accessor.get(ITerminalService).activeInstance?.navigationMode?.focusNextPage(); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ id: TerminalCommandId.ClearSelection, title: { value: localize('workbench.action.terminal.clearSelection', "Clear Selection"), original: 'Clear Selection' }, f1: true, @@ -2067,7 +2128,7 @@ export function registerTerminalActions() { super({ id: TerminalCommandId.SizeToContentWidthInstance, title: terminalStrings.toggleSizeToContentWidth, - f1: false, + f1: true, category, precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus) }); @@ -2088,6 +2149,49 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { getCommandHistory(accessor).clear(); + clearShellFileHistory(); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.WriteDataToTerminal, + title: { value: localize('workbench.action.terminal.writeDataToTerminal', "Write Data to Terminal"), original: 'Write Data to Terminal' }, + f1: true, + category: CATEGORIES.Developer.value + }); + } + async run(accessor: ServicesAccessor) { + const terminalService = accessor.get(ITerminalService); + const terminalGroupService = accessor.get(ITerminalGroupService); + const quickInputService = accessor.get(IQuickInputService); + + const instance = await terminalService.getActiveOrCreateInstance(); + await terminalGroupService.showPanel(); + await instance.processReady; + if (!instance.xterm) { + throw new Error('Cannot write data to terminal if xterm isn\'t initialized'); + } + const data = await quickInputService.input({ + value: '', + placeHolder: 'Enter data, use \\x to escape', + prompt: localize('workbench.action.terminal.writeDataToTerminal.prompt', "Enter data to write directly to the terminal, bypassing the pty"), + }); + if (!data) { + return; + } + let escapedData = data + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r'); + while (true) { + const match = escapedData.match(/\\x([0-9a-fA-F]{2})/); + if (match === null || match.index === undefined || match.length < 2) { + break; + } + escapedData = escapedData.slice(0, match.index) + String.fromCharCode(parseInt(match[1], 16)) + escapedData.slice(match.index + 4); + } + const xterm = instance.xterm as any as IInternalXtermTerminal; + xterm._writeText(escapedData); } }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index 432ea430fa6..6d1f544787c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -244,7 +244,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { ], { sticky: true, - neverShowAgain: { id: 'terminalConfigHelper/launchRecommendationsIgnore', scope: NeverShowAgainScope.GLOBAL }, + neverShowAgain: { id: 'terminalConfigHelper/launchRecommendationsIgnore', scope: NeverShowAgainScope.APPLICATION }, onCancel: () => { } } ); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 71eeff2a7ac..61412cfb781 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -31,6 +31,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { openContextMenu } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; const findWidgetSelector = '.simple-find-part-wrapper'; @@ -68,7 +69,8 @@ export class TerminalEditor extends EditorPane { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @INotificationService private readonly _notificationService: INotificationService, - @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService + @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, + @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService ) { super(terminalEditorId, telemetryService, themeService, storageService); this._findState = new FindReplaceState(); @@ -86,7 +88,7 @@ export class TerminalEditor extends EditorPane { if (this._lastDimension) { this.layout(this._lastDimension); } - this._editorInput.terminalInstance?.setVisible(this.isVisible()); + this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART)); if (this._editorInput.terminalInstance) { // since the editor does not monitor focus changes, for ex. between the terminal // panel and the editors, this is needed so that the active instance gets set @@ -208,7 +210,7 @@ export class TerminalEditor extends EditorPane { override setVisible(visible: boolean, group?: IEditorGroup): void { super.setVisible(visible, group); - return this._editorInput?.terminalInstance?.setVisible(visible); + this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART)); } override getActionViewItem(action: IAction): IActionViewItem | undefined { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts index 77e38655595..aec68ee5d91 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts @@ -101,6 +101,14 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor } } })); + this._register(this._editorService.onDidActiveEditorChange(() => { + const instance = this._editorService.activeEditor instanceof TerminalEditorInput ? this._editorService.activeEditor : undefined; + if (!instance) { + for (const instance of this.instances) { + instance.resetFocusContextKey(); + } + } + })); } private _getActiveTerminalEditors(): EditorInput[] { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEscapeSequences.ts b/src/vs/workbench/contrib/terminal/browser/terminalEscapeSequences.ts new file mode 100644 index 00000000000..7d873ca534f --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalEscapeSequences.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * The identifier for the first numeric parameter (`Ps`) for OSC commands used by shell integration. + */ +const enum ShellIntegrationOscPs { + /** + * Sequences pioneered by FinalTerm. + */ + FinalTerm = 133, + /** + * Sequences pioneered by VS Code. The number is derived from the least significant digit of + * "VSC" when encoded in hex ("VSC" = 0x56, 0x53, 0x43). + */ + VSCode = 633, + /** + * Sequences pioneered by iTerm. + */ + ITerm = 1337 +} + +/** + * VS Code-specific shell integration sequences. Some of these are based on common alternatives like + * those pioneered in FinalTerm. The decision to move to entirely custom sequences was to try to + * improve reliability and prevent the possibility of applications confusing the terminal. + */ +export const enum VSCodeOscPt { + /** + * The start of the prompt, this is expected to always appear at the start of a line. + * Based on FinalTerm's `OSC 133 ; A ST`. + */ + PromptStart = 'A', + + /** + * The start of a command, ie. where the user inputs their command. + * Based on FinalTerm's `OSC 133 ; B ST`. + */ + CommandStart = 'B', + + /** + * Sent just before the command output begins. + * Based on FinalTerm's `OSC 133 ; C ST`. + */ + CommandExecuted = 'C', + + /** + * Sent just after a command has finished. The exit code is optional, when not specified it + * means no command was run (ie. enter on empty prompt or ctrl+c). + * Based on FinalTerm's `OSC 133 ; D [; <ExitCode>] ST`. + */ + CommandFinished = 'D', + + /** + * Explicitly set the command line. This helps workaround problems with conpty not having a + * passthrough mode by providing an option on Windows to send the command that was run. With + * this sequence there's no need for the guessing based on the unreliable cursor positions that + * would otherwise be required. + */ + CommandLine = 'E', + + /** + * Similar to prompt start but for line continuations. + */ + ContinuationStart = 'F', + + /** + * Similar to command start but for line continuations. + */ + ContinuationEnd = 'G', + + /** + * The start of the right prompt. + */ + RightPromptStart = 'H', + + /** + * The end of the right prompt. + */ + RightPromptEnd = 'I', + + /** + * Set an arbitrary property: `OSC 633 ; P ; <Property>=<Value> ST`, only known properties will + * be handled. + */ + Property = 'P' +} + +export const enum VSCodeOscProperty { + Task = 'Task' +} + +/** + * ITerm sequences + */ +export const enum ITermOscPt { + /** + * Based on ITerm's `OSC 1337 ; SetMark` sets a mark on the scrollbar + */ + SetMark = 'SetMark' +} + +export function VSCodeSequence(osc: VSCodeOscPt, data?: string | VSCodeOscProperty): string { + return `\x1b]${ShellIntegrationOscPs.VSCode};${osc};${data}\x07`; +} + +export function ITermSequence(osc: ITermOscPt, data?: string): string { + return `\x1b]${ShellIntegrationOscPs.ITerm};${osc};${data}\x07`; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index ec9e1af2191..ec5a3cb0aa9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -123,9 +123,7 @@ export class TerminalFindWidget extends SimpleFindWidget { protected _onFocusTrackerFocus() { const instance = this._terminalService.activeInstance; - if (instance) { - instance.notifyFindWidgetFocusChanged(true); - } + instance?.notifyFindWidgetFocusChanged(true); this._findWidgetFocused.set(true); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 20847bea41f..d311e6feb1c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -15,7 +15,17 @@ import { IShellLaunchConfig, ITerminalTabLayoutInfoById } from 'vs/platform/term import { TerminalStatus } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { getPartByLocation } from 'vs/workbench/browser/parts/views/viewsService'; -const SPLIT_PANE_MIN_SIZE = 120; +const enum Constants { + /** + * The minimum size in pixels of a split pane. + */ + SplitPaneMinSize = 120, + /** + * The number of cells the terminal gets added or removed when asked to increase or decrease + * the view size. + */ + ResizePartCellCount = 4 +} class SplitPaneContainer extends Disposable { private _height: number; @@ -91,10 +101,10 @@ class SplitPaneContainer extends Disposable { } // Ensure the size is not reduced beyond the minimum, otherwise weird things can happen - if (sizes[index] + amount < SPLIT_PANE_MIN_SIZE) { - amount = SPLIT_PANE_MIN_SIZE - sizes[index]; - } else if (sizes[indexToChange] - amount < SPLIT_PANE_MIN_SIZE) { - amount = sizes[indexToChange] - SPLIT_PANE_MIN_SIZE; + if (sizes[index] + amount < Constants.SplitPaneMinSize) { + amount = Constants.SplitPaneMinSize - sizes[index]; + } else if (sizes[indexToChange] - amount < Constants.SplitPaneMinSize) { + amount = sizes[indexToChange] - Constants.SplitPaneMinSize; } // Apply the size change @@ -207,7 +217,7 @@ class SplitPaneContainer extends Disposable { } class SplitPane implements IView { - minimumSize: number = SPLIT_PANE_MIN_SIZE; + minimumSize: number = Constants.SplitPaneMinSize; maximumSize: number = Number.MAX_VALUE; orientation: Orientation | undefined; @@ -253,7 +263,6 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { private _instanceDisposables: Map<number, IDisposable[]> = new Map(); private _activeInstanceIndex: number = -1; - private _isVisible: boolean = false; get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } @@ -315,8 +324,6 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._splitPaneContainer!.split(instance, parentIndex + 1); } - instance.setVisible(this._isVisible); - this._onInstancesChanged.fire(); } @@ -396,9 +403,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { const newIndex = index < this._terminalInstances.length ? index : this._terminalInstances.length - 1; this.setActiveInstanceByIndex(newIndex); // TODO: Only focus the new instance if the group had focus? - if (this.activeInstance) { - this.activeInstance.focus(true); - } + this.activeInstance?.focus(true); } else if (index < this._activeInstanceIndex) { // Adjust active instance index if needed this._activeInstanceIndex--; @@ -481,7 +486,6 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._initialRelativeSizes = undefined; } } - this.setVisible(this._isVisible); } get title(): string { @@ -514,7 +518,6 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } setVisible(visible: boolean): void { - this._isVisible = visible; if (this._groupElement) { this._groupElement.style.display = visible ? '' : 'none'; } @@ -567,9 +570,9 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { const isHorizontal = (direction === Direction.Left || direction === Direction.Right); const font = this._terminalService.configHelper.getFont(); // TODO: Support letter spacing and line height - const amount = isHorizontal ? font.charWidth : font.charHeight; - if (amount) { - this._splitPaneContainer.resizePane(this._activeInstanceIndex, direction, amount, getPartByLocation(this._terminalLocation)); + const charSize = (isHorizontal ? font.charWidth : font.charHeight); + if (charSize) { + this._splitPaneContainer.resizePane(this._activeInstanceIndex, direction, charSize * Constants.ResizePartCellCount, getPartByLocation(this._terminalLocation)); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index 4e057f1cb4d..1f8f3d43392 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -5,7 +5,7 @@ import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { timeout } from 'vs/base/common/async'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; @@ -75,6 +75,8 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe this.onDidChangeGroups(() => this._terminalGroupCountContextKey.set(this.groups.length)); this._findState = new FindReplaceState(); + + Event.any(this.onDidChangeActiveGroup, this.onDidChangeInstances)(() => this.updateVisibility()); } hidePanel(): void { @@ -114,7 +116,7 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe } private _getIndexFromId(terminalId: number): number { - let terminalIndex = this.instances.findIndex(e => e.instanceId === terminalId); + const terminalIndex = this.instances.findIndex(e => e.instanceId === terminalId); if (terminalIndex === -1) { throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`); } @@ -229,14 +231,23 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe this._onDidChangeGroups.fire(); } - // Adjust focus if the group was active - if (wasActiveGroup && this.groups.length > 0) { - const newIndex = index < this.groups.length ? index : this.groups.length - 1; - this.setActiveGroupByIndex(newIndex, true); - this.activeInstance?.focus(true); - } else if (this.activeGroupIndex >= this.groups.length) { - const newIndex = this.groups.length - 1; - this.setActiveGroupByIndex(newIndex); + if (wasActiveGroup) { + // Adjust focus if the group was active + if (this.groups.length > 0) { + const newIndex = index < this.groups.length ? index : this.groups.length - 1; + this.setActiveGroupByIndex(newIndex, true); + this.activeInstance?.focus(true); + } + } else { + // Adjust the active group if the removed group was above the active group + if (this.activeGroupIndex > index) { + this.setActiveGroupByIndex(this.activeGroupIndex - 1); + } + } + // Ensure the active group is still valid, this should set the activeGroupIndex to -1 if + // there are no groups + if (this.activeGroupIndex >= this.groups.length) { + this.setActiveGroupByIndex(this.groups.length - 1); } this._onDidChangeInstances.fire(); @@ -271,7 +282,6 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe const oldActiveGroup = this.activeGroup; this.activeGroupIndex = index; if (force || oldActiveGroup !== this.activeGroup) { - this.groups.forEach((g, i) => g.setVisible(i === this.activeGroupIndex)); this._onDidChangeActiveGroup.fire(this.activeGroup); this._onDidChangeActiveInstance.fire(this.activeInstance); } @@ -309,8 +319,6 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe this.activeGroupIndex = instanceLocation.groupIndex; this._onDidChangeActiveGroup.fire(this.activeGroup); instanceLocation.group.setActiveInstanceByIndex(activeInstanceIndex, true); - this.groups.forEach((g, i) => g.setVisible(i === instanceLocation.groupIndex)); - } setActiveGroupToNext() { @@ -476,6 +484,17 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe return `${index + 1}: ${group.title ? group.title : ''}`; }); } + + /** + * Visibility should be updated in the following cases: + * 1. Toggle `TERMINAL_VIEW_ID` visibility + * 2. Change active group + * 3. Change instances in active group + */ + updateVisibility() { + const visible = this._viewsService.isViewVisible(TERMINAL_VIEW_ID); + this.groups.forEach((g, i) => g.setVisible(visible && i === this.activeGroupIndex)); + } } interface IInstanceLocation { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 61c2efbb885..402e8cf18e8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -35,6 +35,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILogService } from 'vs/platform/log/common/log'; @@ -42,13 +43,15 @@ import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notif import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IProcessDataEvent, IProcessPropertyMap, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ProcessPropertyType, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType } from 'vs/platform/terminal/common/terminal'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { IProcessDataEvent, IProcessPropertyMap, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, PosixShellType, ProcessPropertyType, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType } from 'vs/platform/terminal/common/terminal'; import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/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'; @@ -66,21 +69,23 @@ import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/wid import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon'; import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon'; import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; -import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; -import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { getCommandHistory, getDirectoryHistory } from 'vs/workbench/contrib/terminal/common/history'; -import { DEFAULT_COMMANDS_TO_SKIP_SHELL, INavigationMode, ITerminalBackend, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, ShellIntegrationExitCode, TerminalCommandId, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IEnvironmentVariableCollection, IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { deserializeEnvironmentVariableCollections } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { getCommandHistory, getDirectoryHistory, getShellFileHistory } from 'vs/workbench/contrib/terminal/common/history'; +import { DEFAULT_COMMANDS_TO_SKIP_SHELL, INavigationMode, ITerminalBackend, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TerminalCommandId, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { formatMessageForTerminal, terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; +import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; +import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import type { ITerminalAddon, Terminal as XTermTerminal } from 'xterm'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import type { IMarker, ITerminalAddon, Terminal as XTermTerminal } from 'xterm'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IGenericMarkProperties } from 'vs/platform/terminal/common/terminalProcess'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const enum Constants { /** @@ -121,6 +126,8 @@ interface IGridDimensions { rows: number; } +const shellIntegrationSupportedShellTypes = [PosixShellType.Bash, PosixShellType.Zsh, PosixShellType.PowerShell, WindowsShellType.PowerShell]; + const scrollbarHeight = 5; class TerminalOutputProvider implements ITextModelContentProvider { @@ -173,6 +180,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _horizontalScrollbar: DomScrollableElement | undefined; private _terminalHasTextContextKey: IContextKey<boolean>; private _terminalA11yTreeFocusContextKey: IContextKey<boolean>; + private _navigationModeActiveContextKey: IContextKey<boolean>; private _cols: number = 0; private _rows: number = 0; private _fixedCols: number | undefined; @@ -206,6 +214,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _userHome?: string; private _hasScrollBar?: boolean; private _target?: TerminalLocation | undefined; + private _disableShellIntegrationReporting: boolean | undefined; + private _usedShellIntegrationInjection: boolean = false; readonly capabilities = new TerminalCapabilityStoreMultiplexer(); readonly statusList: ITerminalStatusList; @@ -220,7 +230,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } this._target = value; } - + get disableShellIntegrationReporting(): boolean { + if (this._disableShellIntegrationReporting === undefined) { + this._disableShellIntegrationReporting = (this.shellLaunchConfig.hideFromUser || this.shellLaunchConfig.executable === undefined || this.shellType === undefined) || !shellIntegrationSupportedShellTypes.includes(this.shellType); + } + return this._disableShellIntegrationReporting; + } get instanceId(): number { return this._instanceId; } get resource(): URI { return this._resource; } get cols(): number { @@ -363,7 +378,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @IEditorService private readonly _editorService: IEditorService, @IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService, @IHistoryService private readonly _historyService: IHistoryService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IOpenerService private readonly _openerService: IOpenerService, + @ICommandService private readonly _commandService: ICommandService ) { super(); @@ -379,7 +396,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); this._fixedRows = _shellLaunchConfig.attachPersistentProcess?.fixedDimensions?.rows; this._fixedCols = _shellLaunchConfig.attachPersistentProcess?.fixedDimensions?.cols; - this._icon = _shellLaunchConfig.attachPersistentProcess?.icon || _shellLaunchConfig.icon; // the resource is already set when it's been moved from another window this._resource = resource || getTerminalUri(this._workspaceContextService.getWorkspace().id, this.instanceId, this.title); @@ -400,6 +416,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._terminalHasTextContextKey = TerminalContextKeys.textSelected.bindTo(this._contextKeyService); this._terminalA11yTreeFocusContextKey = TerminalContextKeys.a11yTreeFocus.bindTo(this._contextKeyService); + this._navigationModeActiveContextKey = TerminalContextKeys.navigationModeActive.bindTo(this._contextKeyService); this._terminalAltBufferActiveContextKey = TerminalContextKeys.altBufferActive.bindTo(this._contextKeyService); this._logService.trace(`terminalInstance#ctor (instanceId: ${this.instanceId})`, this._shellLaunchConfig); @@ -425,10 +442,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Resolve just the icon ahead of time so that it shows up immediately in the tabs. This is // disabled in remote because this needs to be sync and the OS may differ on the remote // which would result in the wrong profile being selected and the wrong icon being - // permanently attached to the terminal. + // permanently attached to the terminal. This also doesn't work when the default profile + // setting is set to null, that's handled after the process is created. if (!this.shellLaunchConfig.executable && !workbenchEnvironmentService.remoteAuthority) { this._terminalProfileResolverService.resolveIcon(this._shellLaunchConfig, OS); } + this._icon = _shellLaunchConfig.attachPersistentProcess?.icon || _shellLaunchConfig.icon; // When a custom pty is used set the name immediately so it gets passed over to the exthost // and is available when Pseudoterminal.open fires. @@ -459,13 +478,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.shellLaunchConfig.args = defaultProfile.args; this.shellLaunchConfig.icon = defaultProfile.icon; this.shellLaunchConfig.color = defaultProfile.color; - this.shellLaunchConfig.env = defaultProfile.env; } await this._createProcess(); // Re-establish the title after reconnect if (this.shellLaunchConfig.attachPersistentProcess) { + this._cwd = this.shellLaunchConfig.attachPersistentProcess.cwd; this.refreshTabLabels(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); this.setShellType(this.shellType); } @@ -554,8 +573,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // The terminal panel needs to have been created to get the real view dimensions if (!this._container) { // Set the fallback dimensions if not - this._cols = 80; - this._rows = 30; + this._cols = Constants.DefaultCols; + this._rows = Constants.DefaultRows; return; } @@ -657,7 +676,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { throw new ErrorNoTelemetry('Terminal disposed of during xterm.js creation'); } - const xterm = this._instantiationService.createInstance(XtermTerminal, Terminal, this._configHelper, this._cols, this._rows, this.target || TerminalLocation.Panel, this.capabilities); + const xterm = this._instantiationService.createInstance(XtermTerminal, Terminal, this._configHelper, this._cols, this._rows, this.target || TerminalLocation.Panel, this.capabilities, this.disableShellIntegrationReporting); this.xterm = xterm; const lineDataEventAddon = new LineDataEventAddon(); this.xterm.raw.loadAddon(lineDataEventAddon); @@ -738,6 +757,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._pathService.userHome().then(userHome => { this._userHome = userHome.fsPath; }); + + if (this._isVisible) { + this._open(); + } + return xterm; } @@ -756,25 +780,28 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - async showLinkQuickpick(): Promise<void> { + async showLinkQuickpick(extended?: boolean): Promise<void> { if (!this._terminalLinkQuickpick) { this._terminalLinkQuickpick = this._instantiationService.createInstance(TerminalLinkQuickpick); + this._terminalLinkQuickpick.onDidRequestMoreLinks(() => { + this.showLinkQuickpick(true); + }); } - const links = await this._getLinks(); + const links = await this._getLinks(extended); if (!links) { return; } return await this._terminalLinkQuickpick.show(links); } - private async _getLinks(): Promise<IDetectedLinks | undefined> { + private async _getLinks(extended?: boolean): Promise<IDetectedLinks | undefined> { if (!this.areLinksReady || !this._linkManager) { throw new Error('terminal links are not ready, cannot generate link quick pick'); } if (!this.xterm) { throw new Error('no xterm'); } - return this._linkManager.getLinks(); + return this._linkManager.getLinks(extended); } async openRecentLink(type: 'localFile' | 'url'): Promise<void> { @@ -791,6 +818,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (!this.xterm) { return; } + let placeholder: string; type Item = IQuickPickItem & { command?: ITerminalCommand }; let items: (Item | IQuickPickItem | IQuickPickSeparator)[] = []; const commandMap: Set<string> = new Set(); @@ -801,6 +829,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }; if (type === 'command') { + placeholder = isMacintosh ? nls.localize('selectRecentCommandMac', 'Select a command to run (hold Option-key to edit the command)') : nls.localize('selectRecentCommand', 'Select a command to run (hold Alt-key to edit the command)'); const cmdDetection = this.capabilities.get(TerminalCapability.CommandDetection); const commands = cmdDetection?.commands; // Current session history @@ -870,15 +899,35 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { label, buttons: [removeFromCommandHistoryButton] }); + commandMap.add(label); } } + if (previousSessionItems.length > 0) { items.push( { type: 'separator', label: terminalStrings.previousSessionCategory }, ...previousSessionItems ); } + + // Gather shell file history + const shellFileHistory = await this._instantiationService.invokeFunction(getShellFileHistory, this._shellType); + const dedupedShellFileItems: IQuickPickItem[] = []; + for (const label of shellFileHistory) { + if (!commandMap.has(label)) { + dedupedShellFileItems.unshift({ label }); + } + } + if (dedupedShellFileItems.length > 0) { + items.push( + { type: 'separator', label: nls.localize('shellFileHistoryCategory', '{0} history', this._shellType) }, + ...dedupedShellFileItems + ); + } } else { + placeholder = isMacintosh + ? nls.localize('selectRecentDirectoryMac', 'Select a directory to go to (hold Option-key to edit the command)') + : nls.localize('selectRecentDirectory', 'Select a directory to go to (hold Alt-key to edit the command)'); const cwds = this.capabilities.get(TerminalCapability.CwdDetection)?.cwds || []; if (cwds && cwds.length > 0) { for (const label of cwds) { @@ -913,6 +962,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const outputProvider = this._instantiationService.createInstance(TerminalOutputProvider); const quickPick = this._quickInputService.createQuickPick(); quickPick.items = items; + quickPick.sortByLabel = false; + quickPick.placeholder = placeholder; return new Promise<void>(r => { quickPick.onDidTriggerItemButton(async e => { if (e.button === removeFromCommandHistoryButton) { @@ -941,9 +992,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } quickPick.hide(); }); - quickPick.onDidAccept(e => { + quickPick.onDidAccept(() => { const result = quickPick.activeItems[0]; - this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, true); + this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, !quickPick.keyMods.alt); quickPick.hide(); }); quickPick.show(); @@ -956,7 +1007,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._container = undefined; } - attachToElement(container: HTMLElement): Promise<void> | void { + attachToElement(container: HTMLElement): void { // The container did not change, do nothing if (this._container === container) { return; @@ -964,24 +1015,28 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._attachBarrier.open(); - // Attach has not occurred yet - if (!this._wrapperElement) { - return this._attachToElement(container); - } - this.xterm?.attachToElement(this._wrapperElement); - // The container changed, reattach this._container = container; - this._container.appendChild(this._wrapperElement); + if (this._wrapperElement) { + this._container.appendChild(this._wrapperElement); + } setTimeout(() => this._initDragAndDrop(container)); } - private async _attachToElement(container: HTMLElement): Promise<void> { - if (this._wrapperElement) { - throw new Error('The terminal instance has already been attached to a container'); + /** + * Opens the the terminal instance inside the parent DOM element previously set with + * `attachToElement`, you must ensure the parent DOM element is explicitly visible before + * invoking this function as it performs some DOM calculations internally + */ + private _open(): void { + if (this._wrapperElement || !this.xterm) { + return; + } + + if (!this._container || !this._container.isConnected) { + throw new Error('A container element needs to be set with `attachToElement` and be part of the DOM before calling `_open`'); } - this._container = container; this._wrapperElement = document.createElement('div'); this._wrapperElement.classList.add('terminal-wrapper'); const xtermElement = document.createElement('div'); @@ -989,7 +1044,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._container.appendChild(this._wrapperElement); - const xterm = await this._xtermReadyPromise; + const xterm = this.xterm; // Attach the xterm object to the DOM, exposing it to the smoke tests this._wrapperElement.xterm = xterm.raw; @@ -1026,7 +1081,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const EXCLUDED_KEYS = ['RightArrow', 'LeftArrow', 'UpArrow', 'DownArrow', 'Space', 'Meta', 'Control', 'Shift', 'Alt', '', 'Delete', 'Backspace', 'Tab']; // only keep track of input if prompt hasn't already been shown - if (this._storageService.getBoolean(SHOW_TERMINAL_CONFIG_PROMPT_KEY, StorageScope.GLOBAL, true) && + if (this._storageService.getBoolean(SHOW_TERMINAL_CONFIG_PROMPT_KEY, StorageScope.APPLICATION, true) && !EXCLUDED_KEYS.includes(event.key) && !event.ctrlKey && !event.shiftKey && @@ -1038,7 +1093,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // within commandsToSkipShell, either alert or skip processing by xterm.js if (resolveResult && resolveResult.commandId && this._skipTerminalCommands.some(k => k === resolveResult.commandId) && !this._configHelper.config.sendKeybindingsToShell) { // don't alert when terminal is opened or closed - if (this._storageService.getBoolean(SHOW_TERMINAL_CONFIG_PROMPT_KEY, StorageScope.GLOBAL, true) && + if (this._storageService.getBoolean(SHOW_TERMINAL_CONFIG_PROMPT_KEY, StorageScope.APPLICATION, true) && this._hasHadInput && !TERMINAL_CREATION_COMMANDS.includes(resolveResult.commandId)) { this._notificationService.prompt( @@ -1053,7 +1108,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } as IPromptChoice ] ); - this._storageService.store(SHOW_TERMINAL_CONFIG_PROMPT_KEY, false, StorageScope.GLOBAL, StorageTarget.USER); + this._storageService.store(SHOW_TERMINAL_CONFIG_PROMPT_KEY, false, StorageScope.APPLICATION, StorageTarget.USER); } event.preventDefault(); return false; @@ -1108,21 +1163,16 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._register(dom.addDisposableListener(xterm.raw.textarea, 'blur', () => this._setFocus(false))); this._register(dom.addDisposableListener(xterm.raw.textarea, 'focusout', () => this._setFocus(false))); - this._initDragAndDrop(container); + this._initDragAndDrop(this._container); this._widgetManager.attachToElement(screenElement); this._processManager.onProcessReady((e) => { this._linkManager?.setWidgetManager(this._widgetManager); }); - // const computedStyle = window.getComputedStyle(this._container); - // const computedStyle = window.getComputedStyle(this._container.parentElement!); - // const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); - // const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); if (this._lastLayoutDimensions) { this.layout(this._lastLayoutDimensions); } - this.setVisible(this._isVisible); this.updateConfig(); // If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal @@ -1143,6 +1193,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + resetFocusContextKey(): void { + this._terminalFocusContextKey.reset(); + } + private _initDragAndDrop(container: HTMLElement) { this._dndObserver?.dispose(); const dndController = this._instantiationService.createInstance(TerminalInstanceDragAndDropController, container); @@ -1336,7 +1390,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } - let currentText: string = await this._clipboardService.readText(); + const currentText: string = await this._clipboardService.readText(); if (!await this._shouldPasteText(currentText)) { return; } @@ -1350,7 +1404,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } - let currentText: string = await this._clipboardService.readText('selection'); + const currentText: string = await this._clipboardService.readText('selection'); if (!await this._shouldPasteText(currentText)) { return; } @@ -1369,6 +1423,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Send it to the process await this._processManager.write(text); this._onDidInputData.fire(this); + this.xterm?.scrollToBottom(); } async sendPath(originalPath: string, addNewLine: boolean): Promise<void> { @@ -1378,10 +1433,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setVisible(visible: boolean): void { this._isVisible = visible; - if (this._wrapperElement) { - this._wrapperElement.classList.toggle('active', visible); - } + this._wrapperElement?.classList.toggle('active', visible); if (visible && this.xterm) { + this._open(); // Resize to re-evaluate dimensions, this will ensure when switching to a terminal it is // using the most up to date dimensions (eg. when terminal is created in the background // using cached dimensions of a split terminal). @@ -1435,7 +1489,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } protected _createProcessManager(): TerminalProcessManager { - const processManager = this._instantiationService.createInstance(TerminalProcessManager, this._instanceId, this._configHelper, this.shellLaunchConfig?.cwd); + let deserializedCollections: ReadonlyMap<string, IEnvironmentVariableCollection> | undefined; + if (this.shellLaunchConfig.attachPersistentProcess?.environmentVariableCollections) { + deserializedCollections = deserializeEnvironmentVariableCollections(this.shellLaunchConfig.attachPersistentProcess.environmentVariableCollections); + } + const processManager = this._instantiationService.createInstance(TerminalProcessManager, this._instanceId, this._configHelper, this.shellLaunchConfig?.cwd, deserializedCollections); this.capabilities.add(processManager.capabilities); processManager.onProcessReady(async (e) => { this._onProcessIdReady.fire(this); @@ -1490,6 +1548,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { case ProcessPropertyType.HasChildProcesses: this._onDidChangeHasChildProcesses.fire(value); break; + case ProcessPropertyType.UsedShellIntegrationInjection: + this._usedShellIntegrationInjection = true; + break; } }); @@ -1541,22 +1602,29 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._initDimensions(); this.xterm?.raw.resize(this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows); } - - const hadIcon = !!this.shellLaunchConfig.icon; - + const originalIcon = this.shellLaunchConfig.icon; await this._processManager.createProcess(this._shellLaunchConfig, this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows, this._accessibilityService.isScreenReaderOptimized()).then(error => { if (error) { - this._onProcessExit(error, error.code === ShellIntegrationExitCode); + this._onProcessExit(error); } }); if (this.xterm?.shellIntegration) { this.capabilities.add(this.xterm?.shellIntegration.capabilities); } - if (!hadIcon && this.shellLaunchConfig.icon || this.shellLaunchConfig.color) { + if (originalIcon !== this.shellLaunchConfig.icon || this.shellLaunchConfig.color) { + this._icon = this._shellLaunchConfig.attachPersistentProcess?.icon || this._shellLaunchConfig.icon; this._onIconChanged.fire(this); } } + public registerMarker(): IMarker | undefined { + return this.xterm?.raw.registerMarker(); + } + + public addGenericMark(marker: IMarker, genericMarkProperties: IGenericMarkProperties): void { + this.xterm?.addDecoration(marker, genericMarkProperties); + } + private _onProcessData(ev: IProcessDataEvent): void { const messageId = ++this._latestXtermWriteData; if (ev.trackCommit) { @@ -1581,18 +1649,25 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { * @param exitCode The exit code of the process, this is undefined when the terminal was exited * through user action. */ - private async _onProcessExit(exitCodeOrError?: number | ITerminalLaunchError, failedShellIntegrationInjection?: boolean): Promise<void> { + private async _onProcessExit(exitCodeOrError?: number | ITerminalLaunchError): Promise<void> { // Prevent dispose functions being triggered multiple times if (this._isExiting) { return; } + const parsedExitResult = parseExitResult(exitCodeOrError, this.shellLaunchConfig, this._processManager.processState, this._initialCwd); + + if (this._usedShellIntegrationInjection && (this._processManager.processState === ProcessState.KilledDuringLaunch || this._processManager.processState === ProcessState.KilledByProcess)) { + this._relaunchWithShellIntegrationDisabled(parsedExitResult?.message); + this._onExit.fire(exitCodeOrError); + return; + } + this._isExiting = true; await this._flushXtermData(); this._logService.debug(`Terminal process exit (instanceId: ${this.instanceId}) with code ${this._exitCode}`); - const parsedExitResult = parseExitResult(exitCodeOrError, this.shellLaunchConfig, this._processManager.processState, this._initialCwd, failedShellIntegrationInjection); this._exitCode = parsedExitResult?.code; const exitMessage = parsedExitResult?.message; @@ -1603,10 +1678,17 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (this._shellLaunchConfig.waitOnExit && this._processManager.processState !== ProcessState.KilledByUser) { this._xtermReadyPromise.then(xterm => { if (exitMessage) { - xterm.raw.writeln(exitMessage); + xterm.raw.write(formatMessageForTerminal(exitMessage)); } - if (typeof this._shellLaunchConfig.waitOnExit === 'string') { - xterm.raw.write(formatMessageForTerminal(this._shellLaunchConfig.waitOnExit)); + switch (typeof this._shellLaunchConfig.waitOnExit) { + case 'string': + xterm.raw.write(formatMessageForTerminal(this._shellLaunchConfig.waitOnExit, { excludeLeadingNewLine: true })); + break; + case 'function': + if (this.exitCode !== undefined) { + xterm.raw.write(formatMessageForTerminal(this._shellLaunchConfig.waitOnExit(this.exitCode), { excludeLeadingNewLine: true })); + } + break; } // Disable all input if the terminal is exiting and listen for next keypress xterm.raw.options.disableStdin = true; @@ -1633,10 +1715,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } - if (failedShellIntegrationInjection) { - this._telemetryService.publicLog2<{ classification: 'SystemMetaData'; purpose: 'FeatureInsight' }>('terminal/shellIntegrationFailureProcessExit'); - } - // First onExit to consumers, this can happen after the terminal has already been disposed. this._onExit.fire(exitCodeOrError); @@ -1646,6 +1724,31 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + private _relaunchWithShellIntegrationDisabled(exitMessage: string | undefined): void { + this._shellLaunchConfig.ignoreShellIntegration = true; + this.relaunch(); + this.statusList.add({ + id: TerminalStatus.ShellIntegrationAttentionNeeded, + severity: Severity.Warning, + icon: Codicon.warning, + tooltip: (`${exitMessage} ` ?? '') + nls.localize('launchFailed.exitCodeOnlyShellIntegration', 'Disabling shell integration in user settings might help.'), + hoverActions: [{ + commandId: TerminalCommandId.ShellIntegrationLearnMore, + label: nls.localize('shellIntegration.learnMore', "Learn more about shell integration"), + run: () => { + this._openerService.open('https://code.visualstudio.com/docs/editor/integrated-terminal#_shell-integration'); + } + }, { + commandId: 'workbench.action.openSettings', + label: nls.localize('shellIntegration.openSettings', "Open user settings"), + run: () => { + this._commandService.executeCommand('workbench.action.openSettings', 'terminal.integrated.shellIntegration.enabled'); + } + }] + }); + this._telemetryService.publicLog2<{}, { owner: 'meganrogge'; comment: 'Indicates the process exited when created with shell integration args' }>('terminal/shellIntegrationFailureProcessExit'); + } + /** * Ensure write calls to xterm.js have finished before resolving. */ @@ -1698,7 +1801,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.xterm.raw.options.disableStdin = false; this._isExiting = false; } - this.xterm.clearDecorations(); + if (reset) { + this.xterm.clearDecorations(); + } } // Dispose the environment info widget if it exists @@ -1716,7 +1821,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Set the new shell launch config this._shellLaunchConfig = shell; // Must be done before calling _createProcess() - await this._processManager.relaunch(this._shellLaunchConfig, this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows, this._accessibilityService.isScreenReaderOptimized(), reset).then(error => { if (error) { this._onProcessExit(error); @@ -1797,7 +1901,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { updateAccessibilitySupport(): void { const isEnabled = this._accessibilityService.isScreenReaderOptimized(); if (isEnabled) { - this._navigationModeAddon = new NavigationModeAddon(this._terminalA11yTreeFocusContextKey); + this._navigationModeAddon = new NavigationModeAddon(this._terminalA11yTreeFocusContextKey, this._navigationModeActiveContextKey); this.xterm!.raw.loadAddon(this._navigationModeAddon); } else { this._navigationModeAddon?.dispose(); @@ -2101,7 +2205,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // work around for https://github.com/xtermjs/xterm.js/issues/3482 if (isWindows) { for (let i = this.xterm.raw.buffer.active.viewportY; i < this.xterm.raw.buffer.active.length; i++) { - let line = this.xterm.raw.buffer.active.getLine(i); + const line = this.xterm.raw.buffer.active.getLine(i); (line as any)._line.isWrapped = false; } } @@ -2144,7 +2248,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if ( !info || this._configHelper.config.environmentChangesIndicator === 'off' || - this._configHelper.config.environmentChangesIndicator === 'warnonly' && !info.requiresAction + this._configHelper.config.environmentChangesIndicator === 'warnonly' && !info.requiresAction || + this._configHelper.config.environmentChangesIndicator === 'on' && !info.requiresAction ) { this.statusList.remove(TerminalStatus.RelaunchNeeded); this._environmentInfo?.disposable.dispose(); @@ -2496,6 +2601,7 @@ export class TerminalLabelComputer extends Disposable { private readonly _onDidChangeLabel = this._register(new Emitter<{ title: string; description: string }>()); readonly onDidChangeLabel = this._onDidChangeLabel.event; + constructor( private readonly _configHelper: TerminalConfigHelper, private readonly _instance: Pick<ITerminalInstance, 'shellLaunchConfig' | 'cwd' | 'fixedCols' | 'fixedRows' | 'initialCwd' | 'processName' | 'sequence' | 'userHome' | 'workspaceFolder' | 'staticTitle' | 'capabilities' | 'title' | 'description'>, @@ -2552,7 +2658,7 @@ export class TerminalLabelComputer extends Disposable { } //Remove special characters that could mess with rendering - let label = template(labelTemplate, (templateProperties as unknown) as { [key: string]: string | ISeparator | undefined | null }).replace(/[\n\r\t]/g, '').trim(); + const label = template(labelTemplate, (templateProperties as unknown) as { [key: string]: string | ISeparator | undefined | null }).replace(/[\n\r\t]/g, '').trim(); return label === '' && labelType === TerminalLabelType.Title ? (this._instance.processName || '') : label; } @@ -2582,8 +2688,7 @@ export function parseExitResult( exitCodeOrError: ITerminalLaunchError | number | undefined, shellLaunchConfig: IShellLaunchConfig, processState: ProcessState, - initialCwd: string | undefined, - failedShellIntegrationInjection?: boolean + initialCwd: string | undefined ): { code: number | undefined; message: string | undefined } | undefined { // Only return a message if the exit code is non-zero if (exitCodeOrError === undefined || exitCodeOrError === 0) { @@ -2605,13 +2710,7 @@ export function parseExitResult( commandLine += shellLaunchConfig.args.map(a => ` '${a}'`).join(); } } - if (failedShellIntegrationInjection) { - if (commandLine) { - message = nls.localize('launchFailed.exitCodeAndCommandLineShellIntegration', "The terminal process \"{0}\" failed to launch (exit code: {1}). Disabling shell integration with `terminal.integrated.shellIntegration.enabled` might help.", commandLine, code); - } else { - message = nls.localize('launchFailed.exitCodeOnlyShellIntegration', "The terminal process failed to launch (exit code: {0}). Disabling shell integration with `terminal.integrated.shellIntegration.enabled` might help.", code); - } - } else if (processState === ProcessState.KilledDuringLaunch) { + if (processState === ProcessState.KilledDuringLaunch) { if (commandLine) { message = nls.localize('launchFailed.exitCodeAndCommandLine', "The terminal process \"{0}\" failed to launch (exit code: {1}).", commandLine, code); } else { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts b/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts index 25fe7aa8aea..edd1bf0aeda 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts @@ -3,26 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; +import { localize } from 'vs/nls'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { registerLogChannel } from 'vs/workbench/services/output/common/output'; +import { join } from 'vs/base/common/path'; +import { TerminalLogConstants } from 'vs/platform/terminal/common/terminal'; /** * The main contribution for the terminal contrib. This contains calls to other components necessary * to set up the terminal but don't need to be tracked in the long term (where TerminalService would * be more relevant). */ -export class TerminalMainContribution implements IWorkbenchContribution { +export class TerminalMainContribution extends Disposable implements IWorkbenchContribution { constructor( @IEditorResolverService editorResolverService: IEditorResolverService, + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService private readonly _fileService: IFileService, @ILabelService labelService: ILabelService, + @ILogService private readonly _logService: ILogService, @ITerminalService terminalService: ITerminalService, @ITerminalEditorService terminalEditorService: ITerminalEditorService, @ITerminalGroupService terminalGroupService: ITerminalGroupService ) { + super(); + // Register terminal editors editorResolverService.registerEditor( `${Schemas.vscodeTerminal}:/**`, @@ -37,12 +51,10 @@ export class TerminalMainContribution implements IWorkbenchContribution { singlePerResource: true }, ({ resource, options }) => { - let instance = terminalService.getInstanceFromResource(resource); + const instance = terminalService.getInstanceFromResource(resource); if (instance) { const sourceGroup = terminalGroupService.getGroupForInstance(instance); - if (sourceGroup) { - sourceGroup.removeInstance(instance); - } + sourceGroup?.removeInstance(instance); } const resolvedResource = terminalEditorService.resolveResource(instance || resource); const editor = terminalEditorService.getInputFromResource(resolvedResource) || { editor: resolvedResource }; @@ -66,5 +78,13 @@ export class TerminalMainContribution implements IWorkbenchContribution { separator: '' } }); + + // Register log channel + this._registerLogChannel('ptyHostLog', localize('ptyHost', "Pty Host"), URI.file(join(environmentService.logsPath, `${TerminalLogConstants.FileName}.log`))); + } + + private _registerLogChannel(id: string, label: string, file: URI): void { + const promise = registerLogChannel(id, label, file, this._fileService, this._logService); + this._register(toDisposable(() => promise.cancel())); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 7ae7dc3c4fc..3e2725c70ff 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -3,37 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { ProcessState, ITerminalProcessManager, ITerminalConfigHelper, IBeforeProcessDataEvent, ITerminalProfileResolverService, ITerminalBackend } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { Schemas } from 'vs/base/common/network'; -import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { IProcessEnvironment, isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo'; -import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { IEnvironmentVariableInfo, IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, ITerminalDimensions, IProcessReadyEvent, IProcessProperty, ProcessPropertyType, IProcessPropertyMap, ITerminalProcessOptions, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; +import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; -import { IProcessEnvironment, isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; -import { NaiveCwdDetectionCapability } from 'vs/platform/terminal/common/capabilities/naiveCwdDetectionCapability'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { URI } from 'vs/base/common/uri'; +import { NaiveCwdDetectionCapability } from 'vs/platform/terminal/common/capabilities/naiveCwdDetectionCapability'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { FlowControlConstants, IProcessDataEvent, IProcessProperty, IProcessPropertyMap, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalEnvironment, ITerminalLaunchError, ITerminalProcessOptions, ProcessPropertyType, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo'; +import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IEnvironmentVariableCollection, IEnvironmentVariableInfo, IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { serializeEnvironmentVariableCollections } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { IBeforeProcessDataEvent, ITerminalBackend, ITerminalConfigHelper, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState } from 'vs/workbench/contrib/terminal/common/terminal'; +import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -119,6 +121,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce private readonly _instanceId: number, private readonly _configHelper: ITerminalConfigHelper, cwd: string | URI | undefined, + environmentVariableCollections: ReadonlyMap<string, IEnvironmentVariableCollection> | undefined, @IHistoryService private readonly _historyService: IHistoryService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @@ -158,6 +161,13 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } else { this.remoteAuthority = this._workbenchEnvironmentService.remoteAuthority; } + + if (environmentVariableCollections) { + this._extEnvironmentVariableCollection = new MergedEnvironmentVariableCollection(environmentVariableCollections); + this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); + this.environmentVariableInfo = new EnvironmentVariableInfoChangesActive(this._extEnvironmentVariableCollection); + this._onEnvironmentVariableInfoChange.fire(this.environmentVariableInfo); + } } override dispose(immediate: boolean = false): void { @@ -200,7 +210,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._dimensions.rows = rows; this._isScreenReaderModeEnabled = isScreenReaderModeEnabled; - let newProcess: ITerminalChildProcess; + let newProcess: ITerminalChildProcess | undefined; if (shellLaunchConfig.customPtyImplementation) { this._processType = ProcessType.PsuedoTerminal; @@ -241,10 +251,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce if (result) { newProcess = result; } else { - this._logService.trace(`Attach to process failed for terminal ${shellLaunchConfig.attachPersistentProcess}`); - return undefined; + // Warn and just create a new terminal if attach failed for some reason + this._logService.warn(`Attach to process failed for terminal`, shellLaunchConfig.attachPersistentProcess); + shellLaunchConfig.attachPersistentProcess = undefined; } - } else { + } + if (!newProcess) { await this._terminalProfileResolverService.resolveShellLaunchConfig(shellLaunchConfig, { remoteAuthority: this.remoteAuthority, os: this.os @@ -253,7 +265,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce shellIntegration: { enabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) }, - windowsEnableConpty: this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled + windowsEnableConpty: this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled, + environmentVariableCollections: this._extEnvironmentVariableCollection?.collections ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined }; try { newProcess = await backend.createProcess( @@ -283,10 +296,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce if (result) { newProcess = result; } else { - this._logService.trace(`Attach to process failed for terminal ${shellLaunchConfig.attachPersistentProcess}`); - return undefined; + // Warn and just create a new terminal if attach failed for some reason + this._logService.warn(`Attach to process failed for terminal`, shellLaunchConfig.attachPersistentProcess); + shellLaunchConfig.attachPersistentProcess = undefined; } - } else { + } + if (!newProcess) { newProcess = await this._launchLocalProcess(backend, shellLaunchConfig, cols, rows, this.userHome, isScreenReaderModeEnabled, variableResolver); } if (!this._isDisposed) { @@ -324,7 +339,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce if (this._preLaunchInputQueue.length > 0 && this._process) { // Send any queued data that's waiting - newProcess.input(this._preLaunchInputQueue.join('')); + newProcess!.input(this._preLaunchInputQueue.join('')); this._preLaunchInputQueue.length = 0; } }), @@ -335,7 +350,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._hasChildProcesses = value; break; case ProcessPropertyType.FailedShellIntegrationActivation: - this._telemetryService?.publicLog2<{ classification: 'SystemMetaData'; purpose: 'FeatureInsight' }>('terminal/shellIntegrationActivationFailureCustomArgs'); + this._telemetryService?.publicLog2<{}, { owner: 'meganrogge'; comment: 'Indicates shell integration was not activated because of custom args' }>('terminal/shellIntegrationActivationFailureCustomArgs'); break; } this._onDidChangeProperty.fire({ type, value }); @@ -395,6 +410,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); if (!this._isDisposed && !shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; + this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); // For remote terminals, this is a copy of the mergedEnvironmentCollection created on // the remote side. Since the environment collection is synced between the remote and @@ -442,7 +458,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce shellIntegration: { enabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) }, - windowsEnableConpty: this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled + windowsEnableConpty: this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled, + environmentVariableCollections: this._extEnvironmentVariableCollection ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined }; const shouldPersist = this._configHelper.config.enablePersistentSessions && !shellLaunchConfig.isFeatureTerminal; return await backend.createProcess(shellLaunchConfig, initialCwd, cols, rows, this._configHelper.config.unicodeVersion, env, options, shouldPersist); @@ -488,7 +505,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // For normal terminals write a message indicating what happened and relaunch // using the previous shellLaunchConfig const message = localize('ptyHostRelaunch', "Restarting the terminal because the connection to the shell process was lost..."); - this._onProcessData.fire({ data: formatMessageForTerminal(message), trackCommit: false }); + this._onProcessData.fire({ data: formatMessageForTerminal(message, { loudFormatting: true }), trackCommit: false }); await this.relaunch(this._shellLaunchConfig, this._dimensions.cols, this._dimensions.rows, this._isScreenReaderModeEnabled, false); } } @@ -620,6 +637,11 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce private _onEnvironmentVariableCollectionChange(newCollection: IMergedEnvironmentVariableCollection): void { const diff = this._extEnvironmentVariableCollection!.diff(newCollection); if (diff === undefined) { + // If there are no longer differences, remove the stale info indicator + if (this.environmentVariableInfo instanceof EnvironmentVariableInfoStale) { + this.environmentVariableInfo = new EnvironmentVariableInfoChangesActive(this._extEnvironmentVariableCollection!); + this._onEnvironmentVariableInfoChange.fire(this.environmentVariableInfo); + } return; } this.environmentVariableInfo = this._instantiationService.createInstance(EnvironmentVariableInfoStale, diff, this._instanceId); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts index 0fe61abdc4d..af7953a3268 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts @@ -39,7 +39,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider<IPick const terminalGroup = terminalGroups[groupIndex]; for (let terminalIndex = 0; terminalIndex < terminalGroup.terminalInstances.length; terminalIndex++) { const terminal = terminalGroup.terminalInstances[terminalIndex]; - const pick = this._createPick(terminal, terminalIndex, filter, groupIndex); + const pick = this._createPick(terminal, terminalIndex, filter, { groupIndex, groupSize: terminalGroup.terminalInstances.length }); if (pick) { terminalPicks.push(pick); } @@ -79,9 +79,14 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider<IPick return terminalPicks; } - private _createPick(terminal: ITerminalInstance, terminalIndex: number, filter: string, groupIndex?: number): IPickerQuickAccessItem | undefined { + private _createPick(terminal: ITerminalInstance, terminalIndex: number, filter: string, groupInfo?: { groupIndex: number; groupSize: number }): IPickerQuickAccessItem | undefined { const iconId = getIconId(terminal); - const label = groupIndex ? `$(${iconId}) ${groupIndex + 1}.${terminalIndex + 1}: ${terminal.title}` : `$(${iconId}) ${terminalIndex + 1}: ${terminal.title}`; + const index = groupInfo + ? (groupInfo.groupSize > 1 + ? `${groupInfo.groupIndex + 1}.${terminalIndex + 1}` + : `${groupInfo.groupIndex + 1}`) + : `${terminalIndex + 1}`; + const label = `$(${iconId}) ${index}: ${terminal.title}`; const iconClasses: string[] = []; const colorClass = getColorClass(terminal); if (colorClass) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 62d52e82cc9..81d40e49f91 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -11,40 +11,40 @@ import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { IKeyMods } from 'vs/base/parts/quickinput/common/quickInput'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICreateContributedTerminalProfileOptions, IShellLaunchConfig, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { iconForeground } from 'vs/platform/theme/common/colorRegistry'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { VirtualWorkspaceContext } from 'vs/workbench/common/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; import { ICreateTerminalOptions, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { getCwdForSplit } 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'; import { getColorStyleContent, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; +import { TerminalProfileQuickpick } from 'vs/workbench/contrib/terminal/browser/terminalProfileQuickpick'; import { getInstanceFromResource, getTerminalUri, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalBackend, ITerminalProcessExtHostProxy, ITerminalProfileService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalBackend, ITerminalConfigHelper, ITerminalProcessExtHostProxy, ITerminalProfileService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; +import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService, ShutdownReason, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { TerminalProfileQuickpick } from 'vs/workbench/contrib/terminal/browser/terminalProfileQuickpick'; -import { IKeyMods } from 'vs/base/parts/quickinput/common/quickInput'; -import { ILogService } from 'vs/platform/log/common/log'; -import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; -import { getCwdForSplit } from 'vs/workbench/contrib/terminal/browser/terminalActions'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ICommandService } from 'vs/platform/commands/common/commands'; export class TerminalService implements ITerminalService { declare _serviceBrand: undefined; @@ -223,7 +223,7 @@ export class TerminalService implements ITerminalService { if (typeof result === 'string') { return; } - let keyMods: IKeyMods | undefined = result.keyMods; + const keyMods: IKeyMods | undefined = result.keyMods; if (type === 'createInstance') { const activeInstance = this.getDefaultInstanceHost().activeInstance; let instance; @@ -434,7 +434,10 @@ export class TerminalService implements ITerminalService { } } else { // add split terminals to this group - terminalInstance = await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, location: { parentTerminal: terminalInstance } }); + terminalInstance = await this.createTerminal({ + config: { attachPersistentProcess: terminalLayout.terminal! }, + location: { parentTerminal: terminalInstance } + }); } } const activeInstance = this.instances.find(t => { @@ -764,8 +767,6 @@ export class TerminalService implements ITerminalService { group.addInstance(source); this.setActiveInstance(source); await this._terminalGroupService.showPanel(true); - // TODO: Shouldn't this happen automatically? - source.setVisible(true); if (target && side) { const index = group.terminalInstances.indexOf(target) + (side === 'after' ? 1 : 0); @@ -775,7 +776,6 @@ export class TerminalService implements ITerminalService { // Fire events this._onDidChangeInstances.fire(); this._onDidChangeActiveGroup.fire(this._terminalGroupService.activeGroup); - this._terminalGroupService.showPanel(true); this._onDidRequestHideFindWidget.fire(); } @@ -929,11 +929,11 @@ export class TerminalService implements ITerminalService { // Await the initialization of available profiles as long as this is not a pty terminal or a // local terminal in a remote workspace as profile won't be used in those cases and these // terminals need to be launched before remote connections are established. - if (!this._terminalProfileService.availableProfiles) { + if (this._terminalProfileService.availableProfiles.length === 0) { const isPtyTerminal = options?.config && 'customPtyImplementation' in options.config; const isLocalInRemoteTerminal = this._remoteAgentService.getConnection() && URI.isUri(options?.cwd) && options?.cwd.scheme === Schemas.vscodeFileResource; if (!isPtyTerminal && !isLocalInRemoteTerminal) { - await this._terminalProfileService.refreshAvailableProfiles(); + await this._terminalProfileService.profilesReady; } } @@ -997,7 +997,7 @@ export class TerminalService implements ITerminalService { } private async _resolveCwd(shellLaunchConfig: IShellLaunchConfig, splitActiveTerminal: boolean, options?: ICreateTerminalOptions): Promise<void> { - let cwd = shellLaunchConfig.cwd; + const cwd = shellLaunchConfig.cwd; if (!cwd) { if (options?.cwd) { shellLaunchConfig.cwd = options.cwd; @@ -1033,7 +1033,6 @@ export class TerminalService implements ITerminalService { } shellLaunchConfig.parentTerminalId = parent.instanceId; instance = group.split(shellLaunchConfig); - this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex)); } return instance; } @@ -1094,10 +1093,10 @@ export class TerminalService implements ITerminalService { // virtual workspaces if (typeof shellLaunchConfig.cwd !== 'string' && shellLaunchConfig.cwd?.scheme === Schemas.file) { if (VirtualWorkspaceContext.getValue(this._contextKeyService)) { - shellLaunchConfig.initialText = formatMessageForTerminal(nls.localize('localTerminalVirtualWorkspace', "⚠ : This shell is open to a {0}local{1} folder, NOT to the virtual folder", '\x1b[3m', '\x1b[23m'), true); + shellLaunchConfig.initialText = formatMessageForTerminal(nls.localize('localTerminalVirtualWorkspace', "This shell is open to a {0}local{1} folder, NOT to the virtual folder", '\x1b[3m', '\x1b[23m'), { excludeLeadingNewLine: true, loudFormatting: true }); shellLaunchConfig.type = 'Local'; } else if (this._remoteAgentService.getConnection()) { - shellLaunchConfig.initialText = formatMessageForTerminal(nls.localize('localTerminalRemote', "⚠ : This shell is running on your {0}local{1} machine, NOT on the connected remote machine", '\x1b[3m', '\x1b[23m'), true); + shellLaunchConfig.initialText = formatMessageForTerminal(nls.localize('localTerminalRemote', "This shell is running on your {0}local{1} machine, NOT on the connected remote machine", '\x1b[3m', '\x1b[23m'), { excludeLeadingNewLine: true, loudFormatting: true }); shellLaunchConfig.type = 'Local'; } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index b514c5c027a..e7aafbb600e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -22,6 +22,7 @@ export const enum TerminalStatus { Bell = 'bell', Disconnected = 'disconnected', RelaunchNeeded = 'relaunch-needed', + ShellIntegrationAttentionNeeded = 'shell-integration-attention-needed' } export interface ITerminalStatus { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index eec5a502976..147c5e14cb3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -202,7 +202,7 @@ export class TerminalTabbedView extends Disposable { private _getLastListWidth(): number { const widthKey = this._panelOrientation === Orientation.VERTICAL ? TerminalStorageKeys.TabsListWidthVertical : TerminalStorageKeys.TabsListWidthHorizontal; - const storedValue = this._storageService.get(widthKey, StorageScope.GLOBAL); + const storedValue = this._storageService.get(widthKey, StorageScope.PROFILE); if (!storedValue || !parseInt(storedValue)) { // we want to use the min width by default for the vertical orientation bc @@ -262,7 +262,7 @@ export class TerminalTabbedView extends Disposable { } this.rerenderTabs(); const widthKey = this._panelOrientation === Orientation.VERTICAL ? TerminalStorageKeys.TabsListWidthVertical : TerminalStorageKeys.TabsListWidthHorizontal; - this._storageService.store(widthKey, width, StorageScope.GLOBAL, StorageTarget.USER); + this._storageService.store(widthKey, width, StorageScope.PROFILE, StorageTarget.USER); } private _setupSplitView(terminalOuterContainer: HTMLElement): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 0a39093d05e..d9fc17533ed 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -625,7 +625,7 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop<ITerminalInstance> { this._autoFocusInstance = undefined; let sourceInstances: ITerminalInstance[] | undefined; - let promises: Promise<IProcessDetails | undefined>[] = []; + const promises: Promise<IProcessDetails | undefined>[] = []; const resources = getTerminalResourcesFromDragEvent(originalEvent); if (resources) { for (const uri of resources) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index ab95697e2d4..5b4bdf420e2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; export function getShellIntegrationTooltip(instance: ITerminalInstance, markdown: boolean, configurationService: IConfigurationService): string { - if (!configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled)) { + if (!configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) || instance.disableShellIntegrationReporting) { return ''; } const shellIntegrationCapabilities: TerminalCapability[] = []; @@ -24,7 +24,11 @@ export function getShellIntegrationTooltip(instance: ITerminalInstance, markdown if (shellIntegrationCapabilities.length > 0) { shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'} ${localize('shellIntegration.enabled', "Shell integration activated")}`; } else { - shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'} ${localize('shellIntegration.activationFailed', "Shell integration failed to activate")}`; + if (instance.shellLaunchConfig.ignoreShellIntegration) { + shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'} ${localize('launchFailed.exitCodeOnlyShellIntegration', "The terminal process failed to launch. Disabling shell integration with terminal.integrated.shellIntegration.enabled might help.")}`; + } else { + shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'} ${localize('shellIntegration.activationFailed', "Shell integration failed to activate")}`; + } } return shellIntegrationString; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts index 168cbe2fbf2..9c53e0d00f6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -1411,6 +1411,7 @@ export class TypeAheadAddon extends Disposable implements ITerminalAddon { private _sendLatencyStats(stats: PredictionStats) { /* __GDPR__ "terminalLatencyStats" : { + "owner": "Tyriar", "min" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "max" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "median" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index d450e1d6609..f7fc381a88f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -171,12 +171,12 @@ export class TerminalViewPane extends ViewPane { // defer focusing the panel to the focus() call // to prevent overriding preserveFocus for extensions this._terminalGroupService.showPanel(false); - if (hadTerminals) { - this._terminalGroupService.activeGroup?.setVisible(visible); - } } else { - this._terminalGroupService.activeGroup?.setVisible(false); + for (const instance of this._terminalGroupService.instances) { + instance.resetFocusContextKey(); + } } + this._terminalGroupService.updateVisibility(); })); this.layoutBody(this._parentDomElement.offsetHeight, this._parentDomElement.offsetWidth); } @@ -521,7 +521,7 @@ function getSingleTabLabel(instance: ITerminalInstance | undefined, separator: s if (!instance || !instance.title) { return ''; } - let iconClass = ThemeIcon.isThemeIcon(instance.icon) ? instance.icon?.id : Codicon.terminal.id; + const iconClass = ThemeIcon.isThemeIcon(instance.icon) ? instance.icon?.id : Codicon.terminal.id; const label = `$(${icon?.id || iconClass}) ${getSingleTabTitle(instance, separator)}`; const primaryStatus = instance.statusList.primary; diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget.ts index c6af5f5c82a..18062e6d456 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget.ts @@ -59,7 +59,7 @@ export class EnvironmentVariableInfoWidget extends Widget implements ITerminalWi } }); }); - this.onnonbubblingmouseout(this._domNode, () => { + this.onmouseleave(this._domNode, () => { scheduler.cancel(); this._mouseMoveListener?.dispose(); }); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts index 60d82f8436d..08174e3b53c 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts @@ -178,7 +178,7 @@ export class CommandNavigationAddon extends Disposable implements ICommandTracke }); this._navigationDecoration = decoration; if (decoration) { - let isRendered = false; + const isRendered = false; decoration.onRender(element => { if (!isRendered) { // TODO: Remove when https://github.com/xtermjs/xterm.js/issues/3686 is fixed diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index d4dba2076d2..0d033529f59 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -8,7 +8,7 @@ import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal' import { IDecoration, ITerminalAddon, Terminal } from 'xterm'; import * as dom from 'vs/base/browser/dom'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { CommandInvalidationReason, ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; @@ -24,14 +24,17 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { Color } from 'vs/base/common/color'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IGenericMarkProperties } from 'vs/platform/terminal/common/terminalProcess'; const enum DecorationSelector { CommandDecoration = 'terminal-command-decoration', ErrorColor = 'error', - DefaultColor = 'default', + DefaultColor = 'default-color', + Default = 'default', Codicon = 'codicon', XtermDecoration = 'xterm-decoration', - OverviewRuler = 'xterm-decoration-overview-ruler' + OverviewRuler = 'xterm-decoration-overview-ruler', + GenericMarkerIcon = 'codicon-circle-small-filled' } const enum DecorationStyles { @@ -39,7 +42,7 @@ const enum DecorationStyles { MarginLeft = -17, } -interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; exitCode?: number } +interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; exitCode?: number; genericMarkProperties?: IGenericMarkProperties } export class DecorationAddon extends Disposable implements ITerminalAddon { protected _terminal: Terminal | undefined; @@ -106,7 +109,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } this._updateClasses(this._placeholderDecoration?.element); for (const decoration of this._decorations.values()) { - this._updateClasses(decoration.decoration.element, decoration.exitCode); + this._updateClasses(decoration.decoration.element, decoration.exitCode, decoration.genericMarkProperties); } } @@ -114,8 +117,17 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { if (this._commandDetectionListeners) { dispose(this._commandDetectionListeners); } + this.clearDecorations(); + } + + private _clearPlaceholder(): void { this._placeholderDecoration?.dispose(); + this._placeholderDecoration = undefined; + } + + public clearDecorations(): void { this._placeholderDecoration?.marker.dispose(); + this._clearPlaceholder(); for (const value of this._decorations.values()) { value.decoration.dispose(); dispose(value.disposables); @@ -176,9 +188,13 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } })); // Current command invalidated - this._commandDetectionListeners.push(capability.onCurrentCommandInvalidated(() => { - this._placeholderDecoration?.dispose(); - this._placeholderDecoration = undefined; + this._commandDetectionListeners.push(capability.onCurrentCommandInvalidated((request) => { + if (request.reason === CommandInvalidationReason.NoProblemsReported) { + const lastDecoration = Array.from(this._decorations.entries())[this._decorations.size - 1]; + lastDecoration?.[1].decoration.dispose(); + } else if (request.reason === CommandInvalidationReason.Windows) { + this._clearPlaceholder(); + } })); } @@ -188,14 +204,14 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } registerCommandDecoration(command: ITerminalCommand, beforeCommandExecution?: boolean): IDecoration | undefined { - if (!this._terminal) { + if (!this._terminal || (beforeCommandExecution && command.genericMarkProperties)) { return undefined; } if (!command.marker) { throw new Error(`cannot add a decoration for a command ${JSON.stringify(command)} with no marker`); } - this._placeholderDecoration?.dispose(); + this._clearPlaceholder(); let color = command.exitCode === undefined ? defaultColor : command.exitCode ? errorColor : successColor; if (color && typeof color !== 'string') { color = color.toString(); @@ -204,37 +220,48 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } const decoration = this._terminal.registerDecoration({ marker: command.marker, - overviewRulerOptions: beforeCommandExecution ? undefined : { color, position: command.exitCode ? 'right' : 'left' } + overviewRulerOptions: beforeCommandExecution + ? { color, position: 'left' } + : { color, position: command.exitCode ? 'right' : 'left' } }); if (!decoration) { return undefined; } - + if (beforeCommandExecution) { + this._placeholderDecoration = decoration; + } decoration.onRender(element => { if (element.classList.contains(DecorationSelector.OverviewRuler)) { return; } - if (beforeCommandExecution && !this._placeholderDecoration) { - this._placeholderDecoration = decoration; - this._placeholderDecoration.onDispose(() => this._placeholderDecoration = undefined); - } else if (!this._decorations.get(decoration.marker.id)) { + if (!this._decorations.get(decoration.marker.id)) { decoration.onDispose(() => this._decorations.delete(decoration.marker.id)); this._decorations.set(decoration.marker.id, { decoration, - disposables: command.exitCode === undefined ? [] : [this._createContextMenu(element, command), ...this._createHover(element, command)], - exitCode: command.exitCode + disposables: this._createDisposables(element, command), + exitCode: command.exitCode, + genericMarkProperties: command.genericMarkProperties }); } if (!element.classList.contains(DecorationSelector.Codicon) || command.marker?.line === 0) { // first render or buffer was cleared this._updateLayout(element); - this._updateClasses(element, command.exitCode); + this._updateClasses(element, command.exitCode, command.genericMarkProperties); } }); return decoration; } + private _createDisposables(element: HTMLElement, command: ITerminalCommand): IDisposable[] { + if (command.exitCode === undefined && !command.genericMarkProperties) { + return []; + } else if (command.genericMarkProperties) { + return [...this._createHover(element, command)]; + } + return [this._createContextMenu(element, command), ...this._createHover(element, command)]; + } + private _updateLayout(element?: HTMLElement): void { if (!element) { return; @@ -252,7 +279,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } } - private _updateClasses(element?: HTMLElement, exitCode?: number): void { + private _updateClasses(element?: HTMLElement, exitCode?: number, genericMarkProperties?: IGenericMarkProperties): void { if (!element) { return; } @@ -260,8 +287,14 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { element.classList.remove(classes); } element.classList.add(DecorationSelector.CommandDecoration, DecorationSelector.Codicon, DecorationSelector.XtermDecoration); - if (exitCode === undefined) { - element.classList.add(DecorationSelector.DefaultColor); + if (genericMarkProperties) { + element.classList.add(DecorationSelector.DefaultColor, DecorationSelector.GenericMarkerIcon); + if (!genericMarkProperties.hoverMessage) { + //disable the mouse pointer + element.classList.add(DecorationSelector.Default); + } + } else if (exitCode === undefined) { + element.classList.add(DecorationSelector.DefaultColor, DecorationSelector.Default); element.classList.add(`codicon-${this._configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationIcon)}`); } else if (exitCode) { element.classList.add(DecorationSelector.ErrorColor); @@ -288,9 +321,15 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { return; } this._hoverDelayer.trigger(() => { - let hoverContent = `${localize('terminalPromptContextMenu', "Show Command Actions")}...`; + let hoverContent = `${localize('terminalPromptContextMenu', "Show Command Actions")}`; hoverContent += '\n\n---\n\n'; - if (command.exitCode) { + if (command.genericMarkProperties) { + if (command.genericMarkProperties.hoverMessage) { + hoverContent = command.genericMarkProperties.hoverMessage; + } else { + return; + } + } else if (command.exitCode) { if (command.exitCode === -1) { hoverContent += localize('terminalPromptCommandFailed', 'Command executed {0} and failed', fromNow(command.timestamp, true)); } else { @@ -324,10 +363,12 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { 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 }) - }); + if (command.command !== '') { + 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 }) + }); + } actions.push({ class: 'how-does-this-work', tooltip: 'How does this work?', dispose: () => { }, id: 'terminal.howDoesThisWork', label: localize("terminal.howDoesThisWork", 'How does this work?'), enabled: true, run: () => this._openerService.open('https://code.visualstudio.com/docs/editor/integrated-terminal#_shell-integration') diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon.ts index 8487459af57..4554951b6ca 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon.ts @@ -10,9 +10,9 @@ import { INavigationMode } from 'vs/workbench/contrib/terminal/common/terminal'; export class NavigationModeAddon implements INavigationMode, ITerminalAddon { private _terminal: Terminal | undefined; - constructor( - private _navigationModeContextKey: IContextKey<boolean> + private _navigationModeContextKey: IContextKey<boolean>, + private _navigationModeActiveContextKey: IContextKey<boolean> ) { } activate(terminal: Terminal): void { @@ -27,59 +27,56 @@ export class NavigationModeAddon implements INavigationMode, ITerminalAddon { } this._terminal.scrollToBottom(); this._terminal.focus(); + this._navigationModeActiveContextKey.set(false); } - focusPreviousLine(): void { - if (!this._terminal || !this._terminal.element) { + focusPreviousPage(): void { + if (!this._terminal?.buffer.active) { return; } - - // Focus previous row if a row is already focused - if (document.activeElement && document.activeElement.parentElement && document.activeElement.parentElement.classList.contains('xterm-accessibility-tree')) { - const element = <HTMLElement | null>document.activeElement.previousElementSibling; - if (element) { - element.focus(); - const disposable = addDisposableListener(element, 'blur', () => { - this._navigationModeContextKey.set(false); - disposable.dispose(); - }); - this._navigationModeContextKey.set(true); - } - return; + this._navigationModeActiveContextKey.set(true); + if (this._terminal?.buffer.active.viewportY < this._terminal.rows) { + this._terminal.scrollToTop(); + this._focusRow(0); + } else { + this._terminal.scrollLines(-this._terminal.rows); + this._focusLine('current'); } + } - // Ensure a11y tree exists - const treeContainer = this._terminal.element.querySelector('.xterm-accessibility-tree'); - if (!treeContainer) { + focusNextPage(): void { + if (!this._terminal?.buffer.active) { return; } - - // Target is row before the cursor - const targetRow = Math.max(this._terminal.buffer.active.cursorY - 1, 0); - - // Check bounds - if (treeContainer.childElementCount < targetRow) { - return; + this._navigationModeActiveContextKey.set(true); + if (this._terminal.buffer.active.viewportY === this._terminal.buffer.active.baseY) { + this._focusRow(this._terminal.rows - 1); + } else { + this._terminal.scrollLines(this._terminal.rows); + this._focusLine('current'); } + } - // Focus - const element = <HTMLElement>treeContainer.childNodes.item(targetRow); - element.focus(); - const disposable = addDisposableListener(element, 'blur', () => { - this._navigationModeContextKey.set(false); - disposable.dispose(); - }); - this._navigationModeContextKey.set(true); + focusPreviousLine(): void { + this._navigationModeActiveContextKey.set(true); + this._focusLine('previous'); } focusNextLine(): void { - if (!this._terminal || !this._terminal.element) { + this._navigationModeActiveContextKey.set(true); + this._focusLine('next'); + } + + private _focusLine(type: 'previous' | 'next' | 'current'): void { + if (!this._terminal?.element) { return; } - - // Focus previous row if a row is already focused + // Focus row if a row is already focused if (document.activeElement && document.activeElement.parentElement && document.activeElement.parentElement.classList.contains('xterm-accessibility-tree')) { - const element = <HTMLElement | null>document.activeElement.nextElementSibling; + let element = <HTMLElement | null>document.activeElement; + if (type !== 'current') { + element = type === 'previous' ? <HTMLElement | null>document.activeElement.previousElementSibling : <HTMLElement | null>document.activeElement.nextElementSibling; + } if (element) { element.focus(); const disposable = addDisposableListener(element, 'blur', () => { @@ -91,21 +88,33 @@ export class NavigationModeAddon implements INavigationMode, ITerminalAddon { return; } + let targetRow: number; + if (type === 'previous') { + targetRow = Math.max(this._terminal.buffer.active.cursorY - 1, 0); + } else { + targetRow = this._terminal.buffer.active.cursorY; + } + this._focusRow(targetRow); + } + + private _focusRow(targetRow: number): void { + if (!this._terminal) { + return; + } + if (!this._terminal?.element) { + return; + } // Ensure a11y tree exists const treeContainer = this._terminal.element.querySelector('.xterm-accessibility-tree'); if (!treeContainer) { return; } - // Target is cursor row - const targetRow = this._terminal.buffer.active.cursorY; - // Check bounds - if (treeContainer.childElementCount < targetRow) { + if (treeContainer.childElementCount < targetRow || targetRow < 0) { return; } - // Focus row before cursor const element = <HTMLElement>treeContainer.childNodes.item(targetRow); element.focus(); const disposable = addDisposableListener(element, 'blur', () => { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 6849cc1ff84..88a89cb7a6c 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IBuffer, ITheme, RendererType, Terminal as RawXtermTerminal } from 'xterm'; +import type { IBuffer, IMarker, ITheme, RendererType, Terminal as RawXtermTerminal } from 'xterm'; import type { ISearchOptions, SearchAddon as SearchAddonType } from 'xterm-addon-search'; import type { Unicode11Addon as Unicode11AddonType } from 'xterm-addon-unicode11'; import type { WebglAddon as WebglAddonType } from 'xterm-addon-webgl'; @@ -16,7 +16,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IShellIntegration, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ITerminalFont, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { isSafari } from 'vs/base/browser/browser'; -import { ICommandTracker, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICommandTracker, IInternalXtermTerminal, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; @@ -32,9 +32,10 @@ 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, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IGenericMarkProperties } from 'vs/platform/terminal/common/terminalProcess'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -50,7 +51,7 @@ let SerializeAddon: typeof SerializeAddonType; * Wraps the xterm object with additional functionality. Interaction with the backing process is out * of the scope of this class. */ -export class XtermTerminal extends DisposableStore implements IXtermTerminal { +export class XtermTerminal extends DisposableStore implements IXtermTerminal, IInternalXtermTerminal { /** The raw xterm.js instance */ readonly raw: RawXtermTerminal; @@ -61,7 +62,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { // Always on addons private _commandNavigationAddon: CommandNavigationAddon; private _shellIntegrationAddon: ShellIntegrationAddon; - private _decorationAddon: DecorationAddon | undefined; + private _decorationAddon: DecorationAddon; // Optional addons private _searchAddon?: SearchAddonType; @@ -99,6 +100,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { rows: number, location: TerminalLocation, private readonly _capabilities: ITerminalCapabilityStore, + disableShellIntegrationReporting: boolean, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @@ -154,17 +156,13 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { if (e.affectsConfiguration(TerminalSettingId.UnicodeVersion)) { this._updateUnicodeVersion(); } - if (e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationsEnabled) || - e.affectsConfiguration(TerminalSettingId.ShellIntegrationEnabled)) { - this._updateShellIntegrationAddons(); - } })); this.add(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); this.add(this._viewDescriptorService.onDidChangeLocation(({ views }) => { if (views.some(v => v.id === TERMINAL_VIEW_ID)) { this._updateTheme(); - this._decorationAddon?.refreshLayouts(); + this._decorationAddon.refreshLayouts(); } })); @@ -175,15 +173,15 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { this._updateUnicodeVersion(); this._commandNavigationAddon = this._instantiationService.createInstance(CommandNavigationAddon, _capabilities); this.raw.loadAddon(this._commandNavigationAddon); - this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon, this._telemetryService); - this.raw.loadAddon(this._shellIntegrationAddon); - this._updateShellIntegrationAddons(); - } - - private _createDecorationAddon(): void { this._decorationAddon = this._instantiationService.createInstance(DecorationAddon, this._capabilities); this._decorationAddon.onDidRequestRunCommand(e => this._onDidRequestRunCommand.fire(e)); this.raw.loadAddon(this._decorationAddon); + this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon, disableShellIntegrationReporting, this._telemetryService); + this.raw.loadAddon(this._shellIntegrationAddon); + } + + addDecoration(marker: IMarker, properties: IGenericMarkProperties): void { + this._capabilities.get(TerminalCapability.CommandDetection)?.handleGenericCommand({ genericMarkProperties: properties, marker }); } async getSelectionAsHtml(command?: ITerminalCommand): Promise<string> { @@ -257,8 +255,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { } clearDecorations(): void { - this._decorationAddon?.dispose(); - this._decorationAddon = undefined; + this._decorationAddon?.clearDecorations(); } forceRefresh() { @@ -396,8 +393,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { clearBuffer(): void { this.raw.clear(); - // hack so that the next placeholder shows - this._decorationAddon?.registerCommandDecoration({ marker: this.raw.registerMarker(0), hasOutput: false, timestamp: Date.now(), getOutput: () => { return undefined; }, command: '' }, true); + // xterm.js does not clear the first prompt, so trigger these to simulate + // the prompt being written + this._capabilities.get(TerminalCapability.CommandDetection)?.handlePromptStart(); + this._capabilities.get(TerminalCapability.CommandDetection)?.handleCommandStart(); } private _setCursorBlink(blink: boolean): void { @@ -450,7 +449,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { // }, 5000); } catch (e) { this._logService.warn(`Webgl could not be loaded. Falling back to the canvas renderer type.`, e); - const neverMeasureRenderTime = this._storageService.getBoolean(TerminalStorageKeys.NeverMeasureRenderTime, StorageScope.GLOBAL, false); + const neverMeasureRenderTime = this._storageService.getBoolean(TerminalStorageKeys.NeverMeasureRenderTime, StorageScope.APPLICATION, false); // if it's already set to dom, no need to measure render time if (!neverMeasureRenderTime && this._configHelper.config.gpuAcceleration !== 'off') { this._measureRenderTime(); @@ -527,7 +526,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { { label: localize('dontShowAgain', "Don't Show Again"), isSecondary: true, - run: () => this._storageService.store(TerminalStorageKeys.NeverMeasureRenderTime, true, StorageScope.GLOBAL, StorageTarget.MACHINE) + run: () => this._storageService.store(TerminalStorageKeys.NeverMeasureRenderTime, true, StorageScope.APPLICATION, StorageTarget.MACHINE) } as IPromptChoice ]; this._notificationService.prompt( @@ -610,20 +609,8 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { } } - private _updateShellIntegrationAddons(): void { - const shellIntegrationEnabled = this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled); - const decorationsEnabled = this._configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationsEnabled); - if (shellIntegrationEnabled) { - if (decorationsEnabled && !this._decorationAddon) { - this._createDecorationAddon(); - } else if (this._decorationAddon && !decorationsEnabled) { - this._decorationAddon.dispose(); - this._decorationAddon = undefined; - } - } - if (this._decorationAddon) { - this._decorationAddon.dispose(); - this._decorationAddon = undefined; - } + // eslint-disable-next-line @typescript-eslint/naming-convention + _writeText(data: string): void { + this.raw.write(data); } } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts index 1f105bfaa94..00a87326b6e 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts @@ -45,6 +45,7 @@ export interface IMergedEnvironmentVariableCollectionDiff { * together. */ export interface IMergedEnvironmentVariableCollection { + readonly collections: ReadonlyMap<string, IEnvironmentVariableCollection>; readonly map: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>; /** @@ -99,6 +100,9 @@ export interface IEnvironmentVariableService { /** [variable, mutator] */ export type ISerializableEnvironmentVariableCollection = [string, IEnvironmentVariableMutator][]; +/** [extension, collection] */ +export type ISerializableEnvironmentVariableCollections = [string, ISerializableEnvironmentVariableCollection][]; + export interface IEnvironmentVariableInfo { readonly requiresAction: boolean; getInfo(): string; diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts index e67f607c506..3aa772dff65 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts @@ -10,7 +10,9 @@ import { VariableResolver } from 'vs/workbench/contrib/terminal/common/terminalE export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection { readonly map: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map(); - constructor(collections: Map<string, IEnvironmentVariableCollection>) { + constructor( + readonly collections: ReadonlyMap<string, IEnvironmentVariableCollection> + ) { collections.forEach((collection, extensionIdentifier) => { const it = collection.map.entries(); let next = it.next(); diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableShared.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableShared.ts index ed39f2e8acd..9295b2af99e 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableShared.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableShared.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEnvironmentVariableMutator, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { IEnvironmentVariableCollection, IEnvironmentVariableMutator, ISerializableEnvironmentVariableCollection, ISerializableEnvironmentVariableCollections } from 'vs/workbench/contrib/terminal/common/environmentVariable'; // This file is shared between the renderer and extension host @@ -16,3 +16,17 @@ export function deserializeEnvironmentVariableCollection( ): Map<string, IEnvironmentVariableMutator> { return new Map<string, IEnvironmentVariableMutator>(serializedCollection); } + +export function serializeEnvironmentVariableCollections(collections: ReadonlyMap<string, IEnvironmentVariableCollection>): ISerializableEnvironmentVariableCollections { + return Array.from(collections.entries()).map(e => { + return [e[0], serializeEnvironmentVariableCollection(e[1].map)]; + }); +} + +export function deserializeEnvironmentVariableCollections( + serializedCollection: ISerializableEnvironmentVariableCollections +): Map<string, IEnvironmentVariableCollection> { + return new Map<string, IEnvironmentVariableCollection>(serializedCollection.map(e => { + return [e[0], { map: deserializeEnvironmentVariableCollection(e[1]) }]; + })); +} diff --git a/src/vs/workbench/contrib/terminal/common/history.ts b/src/vs/workbench/contrib/terminal/common/history.ts index 13d20adef96..fb25868af81 100644 --- a/src/vs/workbench/contrib/terminal/common/history.ts +++ b/src/vs/workbench/contrib/terminal/common/history.ts @@ -3,12 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { env } from 'vs/base/common/process'; import { Disposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { FileOperationError, FileOperationResult, IFileContent, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { TerminalSettingId, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { PosixShellType, TerminalSettingId, TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal'; +import { URI } from 'vs/base/common/uri'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Schemas } from 'vs/base/common/network'; +import { isWindows, OperatingSystem } from 'vs/base/common/platform'; +import { posix, win32 } from 'vs/base/common/path'; /** * Tracks a list of generic entries. @@ -61,6 +68,42 @@ export function getDirectoryHistory(accessor: ServicesAccessor): ITerminalPersis return directoryHistory; } +// Shell file history loads once per shell per window +const shellFileHistory: Map<TerminalShellType, string[] | null> = new Map(); +export async function getShellFileHistory(accessor: ServicesAccessor, shellType: TerminalShellType): Promise<string[]> { + const cached = shellFileHistory.get(shellType); + if (cached === null) { + return []; + } + if (cached !== undefined) { + return cached; + } + let result: IterableIterator<string> | undefined; + switch (shellType) { + case PosixShellType.Bash: + result = await fetchBashHistory(accessor); + break; + case PosixShellType.PowerShell: + case WindowsShellType.PowerShell: + result = await fetchPwshHistory(accessor); + break; + case PosixShellType.Zsh: + result = await fetchZshHistory(accessor); + break; + default: return []; + } + if (result === undefined) { + shellFileHistory.set(shellType, null); + return []; + } + const array = Array.from(result); + shellFileHistory.set(shellType, array); + return array; +} +export function clearShellFileHistory() { + shellFileHistory.clear(); +} + export class TerminalPersistedHistory<T> extends Disposable implements ITerminalPersistedHistory<T> { private readonly _entries: LRUCache<string, T>; private _timestamp: number = 0; @@ -92,7 +135,7 @@ export class TerminalPersistedHistory<T> extends Disposable implements ITerminal // Listen to cache changes from other windows this._storageService.onDidChangeValue(e => { if (e.key === this._getTimestampStorageKey() && !this._isStale) { - this._isStale = this._storageService.getNumber(this._getTimestampStorageKey(), StorageScope.GLOBAL, 0) !== this._timestamp; + this._isStale = this._storageService.getNumber(this._getTimestampStorageKey(), StorageScope.APPLICATION, 0) !== this._timestamp; } }); } @@ -133,7 +176,7 @@ export class TerminalPersistedHistory<T> extends Disposable implements ITerminal } private _loadState() { - this._timestamp = this._storageService.getNumber(this._getTimestampStorageKey(), StorageScope.GLOBAL, 0); + this._timestamp = this._storageService.getNumber(this._getTimestampStorageKey(), StorageScope.APPLICATION, 0); // Load global entries plus const serialized = this._loadPersistedState(); @@ -145,7 +188,7 @@ export class TerminalPersistedHistory<T> extends Disposable implements ITerminal } private _loadPersistedState(): ISerializedCache<T> | undefined { - const raw = this._storageService.get(this._getEntriesStorageKey(), StorageScope.GLOBAL); + const raw = this._storageService.get(this._getEntriesStorageKey(), StorageScope.APPLICATION); if (raw === undefined || raw.length === 0) { return undefined; } @@ -162,9 +205,9 @@ export class TerminalPersistedHistory<T> extends Disposable implements ITerminal private _saveState() { const serialized: ISerializedCache<T> = { entries: [] }; this._entries.forEach((value, key) => serialized.entries.push({ key, value })); - this._storageService.store(this._getEntriesStorageKey(), JSON.stringify(serialized), StorageScope.GLOBAL, StorageTarget.MACHINE); + this._storageService.store(this._getEntriesStorageKey(), JSON.stringify(serialized), StorageScope.APPLICATION, StorageTarget.MACHINE); this._timestamp = Date.now(); - this._storageService.store(this._getTimestampStorageKey(), this._timestamp, StorageScope.GLOBAL, StorageTarget.MACHINE); + this._storageService.store(this._getTimestampStorageKey(), this._timestamp, StorageScope.APPLICATION, StorageTarget.MACHINE); } private _getHistoryLimit() { @@ -180,3 +223,172 @@ export class TerminalPersistedHistory<T> extends Disposable implements ITerminal return `${StorageKeys.Entries}.${this._storageDataKey}`; } } + +export async function fetchBashHistory(accessor: ServicesAccessor): Promise<IterableIterator<string> | undefined> { + const fileService = accessor.get(IFileService); + const remoteAgentService = accessor.get(IRemoteAgentService); + const remoteEnvironment = await remoteAgentService.getEnvironment(); + if (remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows) { + return undefined; + } + const content = await fetchFileContents(env['HOME'], '.bash_history', false, fileService, remoteAgentService); + if (content === undefined) { + return undefined; + } + // .bash_history does not differentiate wrapped commands from multiple commands. Parse + // the output to get the + const fileLines = content.split('\n'); + const result: Set<string> = new Set(); + let currentLine: string; + let currentCommand: string | undefined = undefined; + let wrapChar: string | undefined = undefined; + for (let i = 0; i < fileLines.length; i++) { + currentLine = fileLines[i]; + if (currentCommand === undefined) { + currentCommand = currentLine; + } else { + currentCommand += `\n${currentLine}`; + } + for (let c = 0; c < currentLine.length; c++) { + if (wrapChar) { + if (currentLine[c] === wrapChar) { + wrapChar = undefined; + } + } else { + if (currentLine[c].match(/['"]/)) { + wrapChar = currentLine[c]; + } + } + } + if (wrapChar === undefined) { + if (currentCommand.length > 0) { + result.add(currentCommand.trim()); + } + currentCommand = undefined; + } + } + + return result.values(); +} + +export async function fetchZshHistory(accessor: ServicesAccessor) { + const fileService = accessor.get(IFileService); + const remoteAgentService = accessor.get(IRemoteAgentService); + const remoteEnvironment = await remoteAgentService.getEnvironment(); + if (remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows) { + return undefined; + } + const content = await fetchFileContents(env['HOME'], '.zsh_history', false, fileService, remoteAgentService); + if (content === undefined) { + return undefined; + } + const fileLines = content.split(/\:\s\d+\:\d+;/); + const result: Set<string> = new Set(); + for (let i = 0; i < fileLines.length; i++) { + const sanitized = fileLines[i].replace(/\\\n/g, '\n').trim(); + if (sanitized.length > 0) { + result.add(sanitized); + } + } + return result.values(); +} + +export async function fetchPwshHistory(accessor: ServicesAccessor) { + const fileService: Pick<IFileService, 'readFile'> = accessor.get(IFileService); + const remoteAgentService: Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'> = accessor.get(IRemoteAgentService); + let folderPrefix: string | undefined; + let filePath: string; + const remoteEnvironment = await remoteAgentService.getEnvironment(); + const isFileWindows = remoteEnvironment?.os === OperatingSystem.Windows || !remoteEnvironment && isWindows; + if (isFileWindows) { + folderPrefix = env['APPDATA']; + filePath = '\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt'; + } else { + folderPrefix = env['HOME']; + filePath = '.local/share/powershell/PSReadline/ConsoleHost_history.txt'; + } + const content = await fetchFileContents(folderPrefix, filePath, isFileWindows, fileService, remoteAgentService); + if (content === undefined) { + return undefined; + } + const fileLines = content.split('\n'); + const result: Set<string> = new Set(); + let currentLine: string; + let currentCommand: string | undefined = undefined; + let wrapChar: string | undefined = undefined; + for (let i = 0; i < fileLines.length; i++) { + currentLine = fileLines[i]; + if (currentCommand === undefined) { + currentCommand = currentLine; + } else { + currentCommand += `\n${currentLine}`; + } + if (!currentLine.endsWith('`')) { + const sanitized = currentCommand.trim(); + if (sanitized.length > 0) { + result.add(sanitized); + } + currentCommand = undefined; + continue; + } + // If the line ends with `, the line may be wrapped. Need to also test the case where ` is + // the last character in the line + for (let c = 0; c < currentLine.length; c++) { + if (wrapChar) { + if (currentLine[c] === wrapChar) { + wrapChar = undefined; + } + } else { + if (currentLine[c].match(/`/)) { + wrapChar = currentLine[c]; + } + } + } + // Having an even number of backticks means the line is terminated + // TODO: This doesn't cover more complicated cases where ` is within quotes + if (!wrapChar) { + const sanitized = currentCommand.trim(); + if (sanitized.length > 0) { + result.add(sanitized); + } + currentCommand = undefined; + } else { + // Remove trailing backtick + currentCommand = currentCommand.replace(/`$/, ''); + wrapChar = undefined; + } + } + + return result.values(); +} + +async function fetchFileContents( + folderPrefix: string | undefined, + filePath: string, + isFileWindows: boolean, + fileService: Pick<IFileService, 'readFile'>, + remoteAgentService: Pick<IRemoteAgentService, 'getConnection'>, +): Promise<string | undefined> { + if (!folderPrefix) { + return undefined; + } + const isRemote = !!remoteAgentService.getConnection()?.remoteAuthority; + const historyFileUri = URI.from({ + scheme: isRemote ? Schemas.vscodeRemote : Schemas.file, + path: (isFileWindows ? win32.join : posix.join)(folderPrefix, filePath) + }); + let content: IFileContent; + try { + content = await fileService.readFile(historyFileUri); + } catch (e: unknown) { + // Handle file not found only + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return undefined; + } + throw e; + } + if (content === undefined) { + return undefined; + } + return content.value.toString(); +} diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 1d3a1aa85db..829b45a66d0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -12,7 +12,7 @@ import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChi import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { IProcessDetails, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; +import { IGenericMarkProperties, IProcessDetails, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITerminalCapabilityStore, IXtermMarker } from 'vs/platform/terminal/common/capabilities/capabilities'; @@ -342,12 +342,15 @@ export interface ITerminalCommand { marker?: IXtermMarker; hasOutput: boolean; getOutput(): string | undefined; + genericMarkProperties?: IGenericMarkProperties; } export interface INavigationMode { exitNavigationMode(): void; focusPreviousLine(): void; focusNextLine(): void; + focusPreviousPage(): void; + focusNextPage(): void; } export interface IBeforeProcessDataEvent { @@ -454,11 +457,6 @@ export interface IStartExtensionTerminalRequest { callback: (error: ITerminalLaunchError | undefined) => void; } -export interface IDefaultShellAndArgsRequest { - useAutomationShell: boolean; - callback: (shell: string, args: string[] | string | undefined) => void; -} - export const QUICK_LAUNCH_PROFILE_CHOICE = 'workbench.action.terminal.profile.choice'; export const enum TerminalCommandId { @@ -473,6 +471,7 @@ export const enum TerminalCommandId { ConfigureTerminalSettings = 'workbench.action.terminal.openSettings', OpenDetectedLink = 'workbench.action.terminal.openDetectedLink', OpenWordLink = 'workbench.action.terminal.openWordLink', + ShellIntegrationLearnMore = 'workbench.action.terminal.learnMore', OpenFileLink = 'workbench.action.terminal.openFileLink', OpenWebLink = 'workbench.action.terminal.openUrlLink', RunRecentCommand = 'workbench.action.terminal.runRecentCommand', @@ -554,7 +553,9 @@ export const enum TerminalCommandId { ToggleFindCaseSensitive = 'workbench.action.terminal.toggleFindCaseSensitive', NavigationModeExit = 'workbench.action.terminal.navigationModeExit', NavigationModeFocusNext = 'workbench.action.terminal.navigationModeFocusNext', + NavigationModeFocusNextPage = 'workbench.action.terminal.navigationModeFocusNextPage', NavigationModeFocusPrevious = 'workbench.action.terminal.navigationModeFocusPrevious', + NavigationModeFocusPreviousPage = 'workbench.action.terminal.navigationModeFocusPreviousPage', ShowEnvironmentInformation = 'workbench.action.terminal.showEnvironmentInformation', SearchWorkspace = 'workbench.action.terminal.searchWorkspace', AttachToSession = 'workbench.action.terminal.attachToSession', @@ -564,6 +565,7 @@ export const enum TerminalCommandId { MoveToTerminalPanel = 'workbench.action.terminal.moveToTerminalPanel', SetDimensions = 'workbench.action.terminal.setDimensions', ClearCommandHistory = 'workbench.action.terminal.clearCommandHistory', + WriteDataToTerminal = 'workbench.action.terminal.writeDataToTerminal', } export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index a90e8a5bf3b..efe3aa79bd3 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -280,8 +280,8 @@ const terminalConfiguration: IConfigurationNode = { markdownEnumDescriptions: [ localize('terminal.integrated.gpuAcceleration.auto', "Let VS Code detect which renderer will give the best experience."), localize('terminal.integrated.gpuAcceleration.on', "Enable GPU acceleration within the terminal."), - localize('terminal.integrated.gpuAcceleration.off', "Disable GPU acceleration within the terminal."), - localize('terminal.integrated.gpuAcceleration.canvas', "Use the fallback canvas renderer within the terminal. This uses a 2d context instead of webgl and may be better on some systems.") + localize('terminal.integrated.gpuAcceleration.off', "Disable GPU acceleration within the terminal. The terminal will render much slower when GPU acceleration is off but it should reliably work on all systems."), + localize('terminal.integrated.gpuAcceleration.canvas', "Use the terminal's fallback canvas renderer which uses a 2d context instead of webgl which may perform better on some systems. Note that some features are limited in the canvas renderer like opaque selection.") ], default: 'auto', description: localize('terminal.integrated.gpuAcceleration', "Controls whether the terminal will leverage the GPU to do its rendering.") @@ -500,12 +500,12 @@ const terminalConfiguration: IConfigurationNode = { ] }, [TerminalSettingId.EnablePersistentSessions]: { - description: localize('terminal.integrated.enablePersistentSessions', "Persist terminal sessions for the workspace across window reloads."), + description: localize('terminal.integrated.enablePersistentSessions', "Persist terminal sessions/history for the workspace across window reloads."), type: 'boolean', default: true }, [TerminalSettingId.PersistentSessionReviveProcess]: { - markdownDescription: localize('terminal.integrated.persistentSessionReviveProcess', "When the terminal process must be shutdown (eg. on window or application close), this determines when the previous terminal session contents should be restored and processes be recreated when the workspace is next opened.\n\nCaveats:\n\n- Restoring of the process current working directory depends on whether it is supported by the shell.\n- Time to persist the session during shutdown is limited, so it may be aborted when using high-latency remote connections."), + markdownDescription: localize('terminal.integrated.persistentSessionReviveProcess', "When the terminal process must be shutdown (eg. on window or application close), this determines when the previous terminal session contents/history should be restored and processes be recreated when the workspace is next opened.\n\nCaveats:\n\n- Restoring of the process current working directory depends on whether it is supported by the shell.\n- Time to persist the session during shutdown is limited, so it may be aborted when using high-latency remote connections."), type: 'string', enum: ['onExit', 'onExitAndWindowClose', 'never'], markdownEnumDescriptions: [ @@ -521,7 +521,7 @@ const terminalConfiguration: IConfigurationNode = { default: true }, [TerminalSettingId.AutoReplies]: { - markdownDescription: localize('terminal.integrated.autoReplies', "A set of messages that when encountered in the terminal will be automatically responded to. Provided the message is specific enough, this can help automate away common responses.\n\nRemarks:\n\n- Use {0} to automatically respond to the terminate batch job prompt on Windows.\n- The message includes escape sequences so the reply might not happen with styled text.\n- Each reply can only happen once every second.\n- Use {1} in the reply to mean the enter key.\n- To unset a default key, set the value to null.\n- Restart VS Code if new don't apply.", '`"Terminate batch job (Y/N)": "\\r"`', '`"\\r"`'), + markdownDescription: localize('terminal.integrated.autoReplies', "A set of messages that when encountered in the terminal will be automatically responded to. Provided the message is specific enough, this can help automate away common responses.\n\nRemarks:\n\n- Use {0} to automatically respond to the terminate batch job prompt on Windows.\n- The message includes escape sequences so the reply might not happen with styled text.\n- Each reply can only happen once every second.\n- Use {1} in the reply to mean the enter key.\n- To unset a default key, set the value to null.\n- Restart VS Code if new don't apply.", '`"Terminate batch job (Y/N)": "Y\\r"`', '`"\\r"`'), type: 'object', additionalProperties: { oneOf: [{ diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 553c0edf9df..72f1792fc71 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -22,6 +22,7 @@ export const enum TerminalContextKeyStrings { TabsMouse = 'terminalTabsMouse', AltBufferActive = 'terminalAltBufferActive', A11yTreeFocus = 'terminalA11yTreeFocus', + NavigationModeActive = 'terminalNavigationModeActive', ViewShowing = 'terminalViewShowing', TextSelected = 'terminalTextSelected', FindVisible = 'terminalFindVisible', @@ -84,6 +85,11 @@ export namespace TerminalContextKeys { /** Whether the user is navigating a terminal's the accessibility tree. */ export const a11yTreeFocus = new RawContextKey<boolean>(TerminalContextKeyStrings.A11yTreeFocus, false, true); + /** + * Whether the user is currently in navigation mode + */ + export const navigationModeActive = new RawContextKey<boolean>(TerminalContextKeyStrings.NavigationModeActive, false, true); + /** Whether text is selected in the active terminal. */ export const textSelected = new RawContextKey<boolean>(TerminalContextKeyStrings.TextSelected, false, localize('terminalTextSelectedContextKey', "Whether text is selected in the active terminal.")); diff --git a/src/vs/workbench/contrib/terminal/common/terminalStrings.ts b/src/vs/workbench/contrib/terminal/common/terminalStrings.ts index db912a1ce03..6b270451b86 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalStrings.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalStrings.ts @@ -6,14 +6,6 @@ import { localize } from 'vs/nls'; /** - * Formats a message from the product to be written to the terminal. - */ -export function formatMessageForTerminal(message: string, excludeLeadingNewLine: boolean = false): string { - // Wrap in bold and ensure it's on a new line - return `${excludeLeadingNewLine ? '' : '\r\n'}\x1b[1m${message}\x1b[0m\n\r`; -} - -/** * An object holding strings shared by multiple parts of the terminal */ export const terminalStrings = { diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts index 323dd56a2c1..96b4f135708 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts @@ -25,7 +25,8 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { hasChildProcesses: true, resolvedShellLaunchConfig: {}, overrideDimensions: undefined, - failedShellIntegrationActivation: false + failedShellIntegrationActivation: false, + usedShellIntegrationInjection: undefined }; private readonly _onProcessData = this._register(new Emitter<IProcessDataEvent | string>()); readonly onProcessData = this._onProcessData.event; diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts index e37c554e6ef..a7559d081f0 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts @@ -81,7 +81,11 @@ suite('TerminalLinkManager', () => { instantiationService.stub(IViewDescriptorService, viewDescriptorService); xterm = new Terminal({ cols: 80, rows: 30 }); - linkManager = instantiationService.createInstance(TestLinkManager, xterm, upcastPartial<ITerminalProcessManager>({}), { + linkManager = instantiationService.createInstance(TestLinkManager, xterm, upcastPartial<ITerminalProcessManager>({ + async getInitialCwd() { + return ''; + } + }), { get<T extends TerminalCapability>(capability: T): ITerminalCapabilityImplMap[T] | undefined { return undefined; } diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts index b6efab0951f..dcbdef59e2c 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts @@ -97,11 +97,30 @@ suite('Workbench - TerminalLinkOpeners', () => { capabilities.add(TerminalCapability.CommandDetection, commandDetection); }); + test('should open single exact match against cwd when searching if it exists', async () => { + localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux); + const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); + opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, Promise.resolve('/initial/cwd'), localFileOpener, localFolderOpener, OperatingSystem.Linux); + fileService.setFiles([ + URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo/bar.txt' }), + URI.from({ scheme: Schemas.file, path: '/initial/cwd/foo2/bar.txt' }) + ]); + await opener.open({ + text: 'foo/bar.txt', + bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } }, + type: TerminalBuiltinLinkType.Search + }); + deepStrictEqual(activationResult, { + link: 'file:///initial/cwd/foo/bar.txt', + source: 'editor' + }); + }); + suite('macOS/Linux', () => { setup(() => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, localFolderOpener, OperatingSystem.Linux); + opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, Promise.resolve(''), localFileOpener, localFolderOpener, OperatingSystem.Linux); }); test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => { @@ -150,7 +169,7 @@ suite('Workbench - TerminalLinkOpeners', () => { setup(() => { localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Windows); const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); - opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, localFolderOpener, OperatingSystem.Windows); + opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, Promise.resolve(''), localFileOpener, localFolderOpener, OperatingSystem.Windows); }); test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => { @@ -177,7 +196,7 @@ suite('Workbench - TerminalLinkOpeners', () => { type: TerminalBuiltinLinkType.Search }); deepStrictEqual(activationResult, { - link: 'file:///c%3A%5CUsers%5Chome%5Cfolder%5Cfile.txt', + link: 'file:///c%3A/Users/home/folder/file.txt', source: 'editor' }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts index 448f145fda2..e7a39a54056 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts @@ -79,25 +79,32 @@ suite('Workbench - TerminalUriLinkDetector', () => { { range: [[16, 1], [29, 1]], text: 'http://bar.foo' } ]); }); - test('should not filtrer out https:// link that exceed 1024 characters', async () => { - // 8 + 101 * 10 = 1018 characters - await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(101)}`, [{ - range: [[1, 1], [58, 13]], - text: `https://${'foobarbaz/'.repeat(101)}` - }]); - // 8 + 102 * 10 = 1028 characters - await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(102)}`, [{ - range: [[1, 1], [68, 13]], - text: `https://${'foobarbaz/'.repeat(102)}` + test('should detect file:// links with :line suffix', async () => { + await assertLink(TerminalBuiltinLinkType.LocalFile, 'file:///c:/folder/file:23', [ + { range: [[1, 1], [25, 1]], text: 'file:///c:/folder/file:23' } + ]); + }); + test('should detect file:// links with :line:col suffix', async () => { + await assertLink(TerminalBuiltinLinkType.LocalFile, 'file:///c:/folder/file:23:10', [ + { range: [[1, 1], [28, 1]], text: 'file:///c:/folder/file:23:10' } + ]); + }); + test('should filter out https:// link that exceed 4096 characters', async () => { + // 8 + 200 * 10 = 2008 characters + await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(200)}`, [{ + range: [[1, 1], [8, 26]], + text: `https://${'foobarbaz/'.repeat(200)}` }]); + // 8 + 450 * 10 = 4508 characters + await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(450)}`, []); }); - test('should filter out file:// links that exceed 1024 characters', async () => { - // 8 + 101 * 10 = 1018 characters - await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(101)}`, [{ - text: `file:///${'foobarbaz/'.repeat(101)}`, - range: [[1, 1], [58, 13]] + test('should filter out file:// links that exceed 4096 characters', async () => { + // 8 + 200 * 10 = 2008 characters + await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(200)}`, [{ + text: `file:///${'foobarbaz/'.repeat(200)}`, + range: [[1, 1], [8, 26]] }]); - // 8 + 102 * 10 = 1028 characters - await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(102)}`, []); + // 8 + 450 * 10 = 4508 characters + await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(450)}`, []); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts index 0c1fc1a9c29..6295ccb2922 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts @@ -161,7 +161,7 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - let configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().lineHeight, 2, 'terminal.integrated.lineHeight should be selected over editor.lineHeight'); }); @@ -179,7 +179,7 @@ suite('Workbench - TerminalConfigHelper', function () { } } }); - let configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); + const configHelper = new TestTerminalConfigHelper(configurationService, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().lineHeight, 1, 'editor.lineHeight should be 1 when terminal.integrated.lineHeight not set'); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts index 20b8a85990e..28f71ad6ebf 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts @@ -104,7 +104,7 @@ suite('Workbench - TerminalProcessManager', () => { instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); const configHelper = instantiationService.createInstance(TerminalConfigHelper); - manager = instantiationService.createInstance(TerminalProcessManager, 1, configHelper, undefined); + manager = instantiationService.createInstance(TerminalProcessManager, 1, configHelper, undefined, undefined); }); teardown(() => { diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts index 7e10e587c25..504d062335c 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.test.ts @@ -140,8 +140,8 @@ let jsdebugProfile = { id: 'extension.js-debug.debugTerminal', title: 'JavaScript Debug Terminal' }; -let powershellPick = { label: 'Powershell', profile: powershellProfile, profileName: powershellProfile.profileName }; -let jsdebugPick = { label: 'Javascript Debug Terminal', profile: jsdebugProfile, profileName: jsdebugProfile.title }; +const powershellPick = { label: 'Powershell', profile: powershellProfile, profileName: powershellProfile.profileName }; +const jsdebugPick = { label: 'Javascript Debug Terminal', profile: jsdebugProfile, profileName: jsdebugProfile.title }; suite('TerminalProfileService', () => { let configurationService: TestConfigurationService; @@ -160,9 +160,9 @@ suite('TerminalProfileService', () => { environmentService = { remoteAuthority: undefined } as IWorkbenchEnvironmentService; instantiationService = new TestInstantiationService(); - let themeService = new TestThemeService(); - let terminalContributionService = new TestTerminalContributionService(); - let contextKeyService = new MockContextKeyService(); + const themeService = new TestThemeService(); + const terminalContributionService = new TestTerminalContributionService(); + const contextKeyService = new MockContextKeyService(); instantiationService.stub(IContextKeyService, contextKeyService); instantiationService.stub(IExtensionService, extensionService); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts index 8e8740ea388..bafbd5e65a8 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts @@ -48,7 +48,7 @@ suite('ShellIntegrationAddon', () => { }); const instantiationService = new TestInstantiationService(); instantiationService.stub(ILogService, NullLogService); - shellIntegrationAddon = instantiationService.createInstance(TestShellIntegrationAddon); + shellIntegrationAddon = instantiationService.createInstance(TestShellIntegrationAddon, undefined, undefined); xterm.loadAddon(shellIntegrationAddon); capabilities = shellIntegrationAddon.capabilities; }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts index eaec5127e56..a35b293a2a9 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts @@ -113,7 +113,7 @@ suite('XtermTerminal', () => { instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); configHelper = instantiationService.createInstance(TerminalConfigHelper); - xterm = instantiationService.createInstance(TestXtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, new TerminalCapabilityStore()); + xterm = instantiationService.createInstance(TestXtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, new TerminalCapabilityStore(), true); TestWebglAddon.shouldThrow = false; TestWebglAddon.isEnabled = false; @@ -130,7 +130,7 @@ suite('XtermTerminal', () => { [PANEL_BACKGROUND]: '#ff0000', [SIDE_BAR_BACKGROUND]: '#00ff00' })); - xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, new TerminalCapabilityStore()); + xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, new TerminalCapabilityStore(), true); strictEqual(xterm.raw.options.theme?.background, '#ff0000'); viewDescriptorService.moveTerminalToLocation(ViewContainerLocation.Sidebar); strictEqual(xterm.raw.options.theme?.background, '#00ff00'); @@ -164,7 +164,7 @@ suite('XtermTerminal', () => { 'terminal.ansiBrightCyan': '#150000', 'terminal.ansiBrightWhite': '#160000', })); - xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, new TerminalCapabilityStore()); + xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, new TerminalCapabilityStore(), true); deepStrictEqual(xterm.raw.options.theme, { background: '#000100', foreground: '#000200', diff --git a/src/vs/workbench/contrib/terminal/test/common/history.test.ts b/src/vs/workbench/contrib/terminal/test/common/history.test.ts index 6b850e10e78..325e352d713 100644 --- a/src/vs/workbench/contrib/terminal/test/common/history.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/history.test.ts @@ -3,12 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { deepStrictEqual, strictEqual } from 'assert'; +import { deepStrictEqual, fail, strictEqual } from 'assert'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Schemas } from 'vs/base/common/network'; +import { join } from 'vs/base/common/path'; +import { isWindows, OperatingSystem } from 'vs/base/common/platform'; +import { env } from 'vs/base/common/process'; +import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IFileService } from 'vs/platform/files/common/files'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ITerminalPersistedHistory, TerminalPersistedHistory } from 'vs/workbench/contrib/terminal/common/history'; +import { fetchBashHistory, fetchPwshHistory, fetchZshHistory, ITerminalPersistedHistory, TerminalPersistedHistory } from 'vs/workbench/contrib/terminal/common/history'; +import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; function getConfig(limit: number) { @@ -23,79 +32,371 @@ function getConfig(limit: number) { }; } -suite('TerminalPersistedHistory', () => { - let history: ITerminalPersistedHistory<number>; - let instantiationService: TestInstantiationService; - let storageService: TestStorageService; - let configurationService: TestConfigurationService; +const expectedCommands = [ + 'single line command', + 'git commit -m "A wrapped line in pwsh history\n\nSome commit description\n\nFixes #xyz"', + 'git status', + 'two "\nline"' +]; - setup(() => { - configurationService = new TestConfigurationService(getConfig(5)); - storageService = new TestStorageService(); - instantiationService = new TestInstantiationService(); - instantiationService.set(IConfigurationService, configurationService); - instantiationService.set(IStorageService, storageService); +suite('Terminal history', () => { + suite('TerminalPersistedHistory', () => { + let history: ITerminalPersistedHistory<number>; + let instantiationService: TestInstantiationService; + let storageService: TestStorageService; + let configurationService: TestConfigurationService; - history = instantiationService.createInstance(TerminalPersistedHistory, 'test'); - }); + setup(() => { + configurationService = new TestConfigurationService(getConfig(5)); + storageService = new TestStorageService(); + instantiationService = new TestInstantiationService(); + instantiationService.set(IConfigurationService, configurationService); + instantiationService.set(IStorageService, storageService); + + history = instantiationService.createInstance(TerminalPersistedHistory, 'test'); + }); + + test('should support adding items to the cache and respect LRU', () => { + history.add('foo', 1); + deepStrictEqual(Array.from(history.entries), [ + ['foo', 1] + ]); + history.add('bar', 2); + deepStrictEqual(Array.from(history.entries), [ + ['foo', 1], + ['bar', 2] + ]); + history.add('foo', 1); + deepStrictEqual(Array.from(history.entries), [ + ['bar', 2], + ['foo', 1] + ]); + }); - test('should support adding items to the cache and respect LRU', () => { - history.add('foo', 1); - deepStrictEqual(Array.from(history.entries), [ - ['foo', 1] - ]); - history.add('bar', 2); - deepStrictEqual(Array.from(history.entries), [ - ['foo', 1], - ['bar', 2] - ]); - history.add('foo', 1); - deepStrictEqual(Array.from(history.entries), [ - ['bar', 2], - ['foo', 1] - ]); + test('should support removing specific items', () => { + history.add('1', 1); + history.add('2', 2); + history.add('3', 3); + history.add('4', 4); + history.add('5', 5); + strictEqual(Array.from(history.entries).length, 5); + history.add('6', 6); + strictEqual(Array.from(history.entries).length, 5); + }); + + test('should limit the number of entries based on config', () => { + history.add('1', 1); + history.add('2', 2); + history.add('3', 3); + history.add('4', 4); + history.add('5', 5); + strictEqual(Array.from(history.entries).length, 5); + history.add('6', 6); + strictEqual(Array.from(history.entries).length, 5); + configurationService.setUserConfiguration('terminal', getConfig(2).terminal); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + strictEqual(Array.from(history.entries).length, 2); + history.add('7', 7); + strictEqual(Array.from(history.entries).length, 2); + configurationService.setUserConfiguration('terminal', getConfig(3).terminal); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); + strictEqual(Array.from(history.entries).length, 2); + history.add('8', 8); + strictEqual(Array.from(history.entries).length, 3); + history.add('9', 9); + strictEqual(Array.from(history.entries).length, 3); + }); + + test('should reload from storage service after recreation', () => { + history.add('1', 1); + history.add('2', 2); + history.add('3', 3); + strictEqual(Array.from(history.entries).length, 3); + const history2 = instantiationService.createInstance(TerminalPersistedHistory, 'test'); + strictEqual(Array.from(history2.entries).length, 3); + }); }); + suite('fetchBashHistory', () => { + let fileScheme: string; + let filePath: string; + const fileContent: string = [ + 'single line command', + 'git commit -m "A wrapped line in pwsh history', + '', + 'Some commit description', + '', + 'Fixes #xyz"', + 'git status', + 'two "', + 'line"' + ].join('\n'); + + let instantiationService: TestInstantiationService; + let remoteConnection: Pick<IRemoteAgentConnection, 'remoteAuthority'> | null = null; + let remoteEnvironment: Pick<IRemoteAgentEnvironment, 'os'> | null = null; - test('should support removing specific items', () => { - history.add('1', 1); - history.add('2', 2); - history.add('3', 3); - history.add('4', 4); - history.add('5', 5); - strictEqual(Array.from(history.entries).length, 5); - history.add('6', 6); - strictEqual(Array.from(history.entries).length, 5); + setup(() => { + instantiationService = new TestInstantiationService(); + instantiationService.stub(IFileService, { + async readFile(resource: URI) { + const expected = URI.from({ scheme: fileScheme, path: filePath }); + strictEqual(resource.scheme, expected.scheme); + strictEqual(resource.path, expected.path); + return { value: VSBuffer.fromString(fileContent) }; + } + } as Pick<IFileService, 'readFile'>); + instantiationService.stub(IRemoteAgentService, { + async getEnvironment() { return remoteEnvironment; }, + getConnection() { return remoteConnection; } + } as Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'>); + }); + + if (!isWindows) { + suite('local', async () => { + let originalEnvValues: { HOME: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'] }; + env['HOME'] = '/home/user'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.bash_history'; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + }); + test('current OS', async () => { + filePath = '/home/user/.bash_history'; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchBashHistory))!), expectedCommands); + }); + }); + } + suite('remote', () => { + let originalEnvValues: { HOME: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'] }; + env['HOME'] = '/home/user'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.bash_history'; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + }); + test('Windows', async () => { + remoteEnvironment = { os: OperatingSystem.Windows }; + strictEqual(await instantiationService.invokeFunction(fetchBashHistory), undefined); + }); + test('macOS', async () => { + remoteEnvironment = { os: OperatingSystem.Macintosh }; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchBashHistory))!), expectedCommands); + }); + test('Linux', async () => { + remoteEnvironment = { os: OperatingSystem.Linux }; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchBashHistory))!), expectedCommands); + }); + }); }); + suite('fetchZshHistory', () => { + let fileScheme: string; + let filePath: string; + const fileContent: string = [ + ': 1655252330:0;single line command', + ': 1655252330:0;git commit -m "A wrapped line in pwsh history\\', + '\\', + 'Some commit description\\', + '\\', + 'Fixes #xyz"', + ': 1655252330:0;git status', + ': 1655252330:0;two "\\', + 'line"' + ].join('\n'); + + let instantiationService: TestInstantiationService; + let remoteConnection: Pick<IRemoteAgentConnection, 'remoteAuthority'> | null = null; + let remoteEnvironment: Pick<IRemoteAgentEnvironment, 'os'> | null = null; - test('should limit the number of entries based on config', () => { - history.add('1', 1); - history.add('2', 2); - history.add('3', 3); - history.add('4', 4); - history.add('5', 5); - strictEqual(Array.from(history.entries).length, 5); - history.add('6', 6); - strictEqual(Array.from(history.entries).length, 5); - configurationService.setUserConfiguration('terminal', getConfig(2).terminal); - configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); - strictEqual(Array.from(history.entries).length, 2); - history.add('7', 7); - strictEqual(Array.from(history.entries).length, 2); - configurationService.setUserConfiguration('terminal', getConfig(3).terminal); - configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any); - strictEqual(Array.from(history.entries).length, 2); - history.add('8', 8); - strictEqual(Array.from(history.entries).length, 3); - history.add('9', 9); - strictEqual(Array.from(history.entries).length, 3); + setup(() => { + instantiationService = new TestInstantiationService(); + instantiationService.stub(IFileService, { + async readFile(resource: URI) { + const expected = URI.from({ scheme: fileScheme, path: filePath }); + strictEqual(resource.scheme, expected.scheme); + strictEqual(resource.path, expected.path); + return { value: VSBuffer.fromString(fileContent) }; + } + } as Pick<IFileService, 'readFile'>); + instantiationService.stub(IRemoteAgentService, { + async getEnvironment() { return remoteEnvironment; }, + getConnection() { return remoteConnection; } + } as Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'>); + }); + + if (!isWindows) { + suite('local', () => { + let originalEnvValues: { HOME: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'] }; + env['HOME'] = '/home/user'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.bash_history'; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + }); + test('current OS', async () => { + filePath = '/home/user/.zsh_history'; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchZshHistory))!), expectedCommands); + }); + }); + } + suite('remote', () => { + let originalEnvValues: { HOME: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'] }; + env['HOME'] = '/home/user'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.zsh_history'; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + }); + test('Windows', async () => { + remoteEnvironment = { os: OperatingSystem.Windows }; + strictEqual(await instantiationService.invokeFunction(fetchZshHistory), undefined); + }); + test('macOS', async () => { + remoteEnvironment = { os: OperatingSystem.Macintosh }; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchZshHistory))!), expectedCommands); + }); + test('Linux', async () => { + remoteEnvironment = { os: OperatingSystem.Linux }; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchZshHistory))!), expectedCommands); + }); + }); }); + suite('fetchPwshHistory', () => { + let fileScheme: string; + let filePath: string; + const fileContent: string = [ + 'single line command', + 'git commit -m "A wrapped line in pwsh history`', + '`', + 'Some commit description`', + '`', + 'Fixes #xyz"', + 'git status', + 'two "`', + 'line"' + ].join('\n'); + + let instantiationService: TestInstantiationService; + let remoteConnection: Pick<IRemoteAgentConnection, 'remoteAuthority'> | null = null; + let remoteEnvironment: Pick<IRemoteAgentEnvironment, 'os'> | null = null; + + setup(() => { + instantiationService = new TestInstantiationService(); + instantiationService.stub(IFileService, { + async readFile(resource: URI) { + const expected = URI.from({ scheme: fileScheme, path: filePath }); + if (resource.scheme !== expected.scheme || resource.fsPath !== expected.fsPath) { + fail(`Unexpected file scheme/path ${resource.scheme} ${resource.fsPath}`); + } + return { value: VSBuffer.fromString(fileContent) }; + } + } as Pick<IFileService, 'readFile'>); + instantiationService.stub(IRemoteAgentService, { + async getEnvironment() { return remoteEnvironment; }, + getConnection() { return remoteConnection; } + } as Pick<IRemoteAgentService, 'getConnection' | 'getEnvironment'>); + }); - test('should reload from storage service after recreation', () => { - history.add('1', 1); - history.add('2', 2); - history.add('3', 3); - strictEqual(Array.from(history.entries).length, 3); - const history2 = instantiationService.createInstance(TerminalPersistedHistory, 'test'); - strictEqual(Array.from(history2.entries).length, 3); + suite('local', async () => { + let originalEnvValues: { HOME: string | undefined; APPDATA: string | undefined }; + setup(() => { + originalEnvValues = { HOME: env['HOME'], APPDATA: env['APPDATA'] }; + env['HOME'] = '/home/user'; + env['APPDATA'] = 'C:\\AppData'; + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + filePath = '/home/user/.zsh_history'; + originalEnvValues = { HOME: env['HOME'], APPDATA: env['APPDATA'] }; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + if (originalEnvValues['APPDATA'] === undefined) { + delete env['APPDATA']; + } else { + env['APPDATA'] = originalEnvValues['APPDATA']; + } + }); + test('current OS', async () => { + if (isWindows) { + filePath = join(env['APPDATA']!, 'Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt'); + } else { + filePath = join(env['HOME']!, '.local/share/powershell/PSReadline/ConsoleHost_history.txt'); + } + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchPwshHistory))!), expectedCommands); + }); + }); + suite('remote', () => { + let originalEnvValues: { HOME: string | undefined; APPDATA: string | undefined }; + setup(() => { + remoteConnection = { remoteAuthority: 'some-remote' }; + fileScheme = Schemas.vscodeRemote; + originalEnvValues = { HOME: env['HOME'], APPDATA: env['APPDATA'] }; + }); + teardown(() => { + if (originalEnvValues['HOME'] === undefined) { + delete env['HOME']; + } else { + env['HOME'] = originalEnvValues['HOME']; + } + if (originalEnvValues['APPDATA'] === undefined) { + delete env['APPDATA']; + } else { + env['APPDATA'] = originalEnvValues['APPDATA']; + } + }); + test('Windows', async () => { + remoteEnvironment = { os: OperatingSystem.Windows }; + env['APPDATA'] = 'C:\\AppData'; + filePath = 'C:\\AppData\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt'; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchPwshHistory))!), expectedCommands); + }); + test('macOS', async () => { + remoteEnvironment = { os: OperatingSystem.Macintosh }; + env['HOME'] = '/home/user'; + filePath = '/home/user/.local/share/powershell/PSReadline/ConsoleHost_history.txt'; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchPwshHistory))!), expectedCommands); + }); + test('Linux', async () => { + remoteEnvironment = { os: OperatingSystem.Linux }; + env['HOME'] = '/home/user'; + filePath = '/home/user/.local/share/powershell/PSReadline/ConsoleHost_history.txt'; + deepStrictEqual(Array.from((await instantiationService.invokeFunction(fetchPwshHistory))!), expectedCommands); + }); + }); }); }); diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 7400d289061..d93803367a4 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -15,6 +15,17 @@ position: relative; } +.test-explorer .test-item .label, +.test-output-peek-tree .test-peek-item .name { + display: flex; + align-items: center; + flex-grow: 1; + gap: 0.15em; + width: 0; + overflow: hidden; + white-space: nowrap; +} + .test-explorer .test-item, .test-output-peek-tree .test-peek-item { display: flex; @@ -210,3 +221,10 @@ .test-message-inline-content-clickable { cursor: pointer; } + +.test-label-description { + opacity: .7; + margin-left: 0.5em; + font-size: .9em; + white-space: pre; +} diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 5bdc3ee229a..718807b102e 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -49,6 +49,7 @@ import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testPro import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { getContextForTestItem, ITestService, testsInFile } from 'vs/workbench/contrib/testing/common/testService'; +import { stripIcons } from 'vs/base/common/iconLabels'; const MAX_INLINE_MESSAGE_LENGTH = 128; @@ -437,7 +438,7 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] } let computedState = TestResultState.Unset; - let hoverMessageParts: string[] = []; + const hoverMessageParts: string[] = []; let testIdWithMessages: string | undefined; for (let i = 0; i < tests.length; i++) { const test = tests[i]; @@ -459,7 +460,7 @@ const createRunTestDecoration = (tests: readonly IncrementalTestCollectionItem[] let hoverMessage: IMarkdownString | undefined; - let glyphMarginClassName = ThemeIcon.asClassName(icon) + ' testing-run-glyph'; + const glyphMarginClassName = ThemeIcon.asClassName(icon) + ' testing-run-glyph'; return { range: firstLineRange(range), @@ -818,7 +819,7 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio const testSubmenus = this.tests.map(({ test, resultItem }) => { const actions = this.getTestContextMenuActions(test, resultItem); disposable.add(actions); - return new SubmenuAction(test.item.extId, test.item.label, actions.object); + return new SubmenuAction(test.item.extId, stripIcons(test.item.label), actions.object); }); return { object: Separator.join(allActions, testSubmenus), dispose: () => disposable.dispose() }; diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index b742efad054..0c639a29c53 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { AbstractTreeViewState, IAbstractTreeViewState } from 'vs/base/browser/ui/tree/abstractTree'; @@ -32,7 +33,6 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { FileKind } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; @@ -44,7 +44,6 @@ import { foreground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IResourceLabel, IResourceLabelOptions, IResourceLabelProps, ResourceLabels } from 'vs/workbench/browser/labels'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IViewDescriptorService } from 'vs/workbench/common/views'; @@ -58,7 +57,6 @@ import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/ import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; 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/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'; @@ -68,6 +66,7 @@ import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { IMainThreadTestCollection, ITestService, testCollectionIsEmpty } from 'vs/workbench/contrib/testing/common/testService'; +import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class TestingExplorerView extends ViewPane { @@ -294,7 +293,7 @@ export class TestingExplorerView extends ViewPane { profileActions.shift(); } - let postActions: IAction[] = []; + const postActions: IAction[] = []; if (profileActions.length > 1) { postActions.push(new Action( 'selectDefaultTestConfigurations', @@ -474,8 +473,6 @@ export class TestingExplorerViewModel extends Disposable { this._viewMode.set(this.storageService.get('testing.viewMode', StorageScope.WORKSPACE, TestExplorerViewMode.Tree) as TestExplorerViewMode); this._viewSorting.set(this.storageService.get('testing.viewSorting', StorageScope.WORKSPACE, TestExplorerViewSorting.ByLocation) as TestExplorerViewSorting); - const labels = this._register(instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: onDidChangeVisibility })); - this.reevaluateWelcomeState(); this.filter = this.instantiationService.createInstance(TestsFilter, testService.collection); this.tree = instantiationService.createInstance( @@ -484,7 +481,7 @@ export class TestingExplorerViewModel extends Disposable { listContainer, new ListDelegate(), [ - instantiationService.createInstance(TestItemRenderer, labels, this.actionRunner), + instantiationService.createInstance(TestItemRenderer, this.actionRunner), instantiationService.createInstance(ErrorRenderer), ], { @@ -557,6 +554,11 @@ export class TestingExplorerViewModel extends Disposable { followRunningTests = getTestingConfiguration(configurationService, TestingConfigKeys.FollowRunningTest); })); + let alwaysRevealTestAfterStateChange = getTestingConfiguration(configurationService, TestingConfigKeys.AlwaysRevealTestOnStateChange); + this._register(configurationService.onDidChangeConfiguration(() => { + alwaysRevealTestAfterStateChange = getTestingConfiguration(configurationService, TestingConfigKeys.AlwaysRevealTestOnStateChange); + })); + this._register(testResults.onTestChanged(evt => { if (!followRunningTests) { return; @@ -572,7 +574,7 @@ export class TestingExplorerViewModel extends Disposable { return; } - this.revealById(evt.item.item.extId, false, false); + this.revealById(evt.item.item.extId, alwaysRevealTestAfterStateChange, false); })); this._register(testResults.onResultsChanged(() => { @@ -1095,7 +1097,7 @@ class ErrorRenderer implements ITreeRenderer<TestTreeErrorMessage, FuzzyScore, I } interface IActionableElementTemplateData { - label: IResourceLabel; + label: HTMLElement; icon: HTMLElement; wrapper: HTMLElement; actionBar: ActionBar; @@ -1106,7 +1108,6 @@ interface IActionableElementTemplateData { abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends Disposable implements ITreeRenderer<T, FuzzyScore, IActionableElementTemplateData> { constructor( - protected readonly labels: ResourceLabels, private readonly actionRunner: TestExplorerActionRunner, @IMenuService private readonly menuService: IMenuService, @ITestService protected readonly testService: ITestService, @@ -1129,8 +1130,7 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends const wrapper = dom.append(container, dom.$('.test-item')); const icon = dom.append(wrapper, dom.$('.computed-state')); - const name = dom.append(wrapper, dom.$('.name')); - const label = this.labels.create(name, { supportHighlights: true }); + const label = dom.append(wrapper, dom.$('.label')); dom.append(wrapper, dom.$(ThemeIcon.asCSSSelector(icons.testingHiddenIcon))); const actionBar = new ActionBar(wrapper, { @@ -1141,7 +1141,7 @@ abstract class ActionableItemTemplateData<T extends TestItemTreeElement> extends : undefined }); - return { wrapper, label, actionBar, icon, elementDisposable: [], templateDisposable: [label, actionBar] }; + return { wrapper, label, actionBar, icon, elementDisposable: [], templateDisposable: [actionBar] }; } /** @@ -1192,10 +1192,6 @@ class TestItemRenderer extends ActionableItemTemplateData<TestItemTreeElement> { public override renderElement(node: ITreeNode<TestItemTreeElement, FuzzyScore>, depth: number, data: IActionableElementTemplateData): void { super.renderElement(node, depth, data); - const label: IResourceLabelProps = { name: node.element.label }; - const options: IResourceLabelOptions = {}; - data.label.setResource(label, options); - const testHidden = this.testService.excluded.contains(node.element.test); data.wrapper.classList.toggle('test-is-hidden', testHidden); @@ -1205,18 +1201,20 @@ class TestItemRenderer extends ActionableItemTemplateData<TestItemTreeElement> { : node.element.state); data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : ''); - label.resource = node.element.test.item.uri; - options.title = getLabelForTestTreeElement(node.element); - options.fileKind = FileKind.FILE; - label.description = node.element.description || undefined; + data.label.title = getLabelForTestTreeElement(node.element); + dom.reset(data.label, ...renderLabelWithIcons(node.element.label)); + + let description = node.element.description; if (node.element.duration !== undefined) { - label.description = label.description - ? `${label.description}: ${formatDuration(node.element.duration)}` + description = description + ? `${description}: ${formatDuration(node.element.duration)}` : formatDuration(node.element.duration); } - data.label.setResource(label, options); + if (description) { + dom.append(data.label, dom.$('span.test-label-description', {}, description)); + } } } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 4c5e6817df5..c3452de68dd 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -7,6 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert } from 'vs/base/browser/ui/aria/aria'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; @@ -20,6 +21,7 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { stripIcons } from 'vs/base/common/iconLabels'; import { Iterable } from 'vs/base/common/iterator'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; @@ -53,7 +55,6 @@ import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listSe import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; @@ -65,7 +66,6 @@ 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/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'; @@ -75,6 +75,7 @@ import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testPro import { ITestResult, maxCountPriority, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; class TestDto { @@ -418,7 +419,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo */ public readonly historyVisible = MutableObservableValue.stored(new StoredValue<boolean>({ key: 'testHistoryVisibleInPeek', - scope: StorageScope.GLOBAL, + scope: StorageScope.PROFILE, target: StorageTarget.USER, }, this.storageService), true); @@ -769,7 +770,7 @@ class TestingOutputPeek extends PeekViewWidget { */ public async showInPlace(dto: TestDto) { const message = dto.messages[dto.messageIndex]; - this.setTitle(firstLine(renderStringAsPlaintext(message.message)), dto.test.label); + this.setTitle(firstLine(renderStringAsPlaintext(message.message)), stripIcons(dto.test.label)); this.didReveal.fire(dto); this.visibilityChange.fire(true); await Promise.all(this.contentProviders.map(p => p.update(dto, message))); @@ -925,7 +926,8 @@ class ScrollableMarkdownMessage extends Disposable { } public layout(height: number, width: number) { - this.scrollable.setScrollDimensions({ width, height }); + // Remove padding of `.monaco-editor .zone-widget.test-output-peek .preview-text` + this.scrollable.setScrollDimensions({ width: width - 32, height: height - 16 }); } } @@ -1083,6 +1085,7 @@ interface ITreeElement { context: unknown; id: string; label: string; + labelWithIcons?: readonly (HTMLSpanElement | string)[]; icon?: ThemeIcon; description?: string; ariaLabel?: string; @@ -1110,6 +1113,7 @@ export class TestCaseElement implements ITreeElement { public readonly context = this.test.item.extId; public readonly id = `${this.results.id}/${this.test.item.extId}`; public readonly label = this.test.item.label; + public readonly labelWithIcons = renderLabelWithIcons(this.label); public readonly description?: string; public get icon() { @@ -1208,7 +1212,6 @@ class OutputPeekTree extends Disposable { super(); this.treeActions = instantiationService.createInstance(TreeActionsProvider); - const labels = instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility }); const diffIdentityProvider: IIdentityProvider<TreeElement> = { getId(e: TreeElement) { return e.id; @@ -1223,7 +1226,7 @@ class OutputPeekTree extends Disposable { getHeight: () => 22, getTemplateId: () => TestRunElementRenderer.ID, }, - [instantiationService.createInstance(TestRunElementRenderer, labels, this.treeActions)], + [instantiationService.createInstance(TestRunElementRenderer, this.treeActions)], { compressionEnabled: true, hideTwistiesOfChildlessElements: true, @@ -1417,7 +1420,7 @@ class OutputPeekTree extends Disposable { } interface TemplateData { - label: IResourceLabel; + label: HTMLElement; icon: HTMLElement; actionBar: ActionBar; elementDisposable: DisposableStore; @@ -1429,7 +1432,6 @@ class TestRunElementRenderer implements ICompressibleTreeRenderer<ITreeElement, public readonly templateId = TestRunElementRenderer.ID; constructor( - private readonly labels: ResourceLabels, private readonly treeActions: TreeActionsProvider, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { } @@ -1450,10 +1452,7 @@ class TestRunElementRenderer implements ICompressibleTreeRenderer<ITreeElement, const templateDisposable = new DisposableStore(); const wrapper = dom.append(container, dom.$('.test-peek-item')); const icon = dom.append(wrapper, dom.$('.state')); - const name = dom.append(wrapper, dom.$('.name')); - - const label = this.labels.create(name, { supportHighlights: true }); - templateDisposable.add(label); + const label = dom.append(wrapper, dom.$('.name')); const actionBar = new ActionBar(wrapper, { actionViewItemProvider: action => @@ -1485,7 +1484,13 @@ class TestRunElementRenderer implements ICompressibleTreeRenderer<ITreeElement, private doRender(element: ITreeElement, templateData: TemplateData) { templateData.elementDisposable.clear(); - templateData.label.setLabel(element.label, element.description); + if (element.labelWithIcons) { + dom.reset(templateData.label, ...element.labelWithIcons); + } else if (element.description) { + dom.reset(templateData.label, element.label, dom.$('span.test-label-description', {}, element.description)); + } else { + dom.reset(templateData.label, element.label); + } const icon = element.icon; templateData.icon.className = `computed-state ${icon ? ThemeIcon.asClassName(icon) : ''}`; diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index e9ab2c0ac56..eabfe6204c1 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -17,6 +17,7 @@ export const enum TestingConfigKeys { DefaultGutterClickAction = 'testing.defaultGutterClickAction', GutterEnabled = 'testing.gutterEnabled', SaveBeforeTest = 'testing.saveBeforeTest', + AlwaysRevealTestOnStateChange = 'testing.alwaysRevealTestOnStateChange' } export const enum AutoOpenTesting { @@ -128,6 +129,11 @@ export const testingConfiguation: IConfigurationNode = { default: 'openOnTestStart', description: localize('testing.openTesting', "Controls when the testing view should open.") }, + [TestingConfigKeys.AlwaysRevealTestOnStateChange]: { + markdownDescription: localize('testing.alwaysRevealTestOnStateChange', "Always reveal the executed test when `#testing.followRunningTest#` is on. If this setting is turned off, only failed tests will be revealed."), + type: 'boolean', + default: false, + }, } }; @@ -141,6 +147,7 @@ export interface ITestingConfiguration { [TestingConfigKeys.GutterEnabled]: boolean; [TestingConfigKeys.SaveBeforeTest]: boolean; [TestingConfigKeys.OpenTesting]: AutoOpenTesting; + [TestingConfigKeys.AlwaysRevealTestOnStateChange]: boolean; } export const getTestingConfiguration = <K extends TestingConfigKeys>(config: IConfigurationService, key: K) => config.getValue<ITestingConfiguration[K]>(key); diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 224cbd48e90..198dc0c9b6a 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { stripIcons } from 'vs/base/common/iconLabels'; import { localize } from 'vs/nls'; import { TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; @@ -44,7 +45,7 @@ export const testStateNames: { [K in TestResultState]: string } = { export const labelForTestInState = (label: string, state: TestResultState) => localize({ key: 'testing.treeElementLabel', comment: ['label then the unit tests state, for example "Addition Tests (Running)"'], -}, '{0} ({1})', label, testStateNames[state]); +}, '{0} ({1})', stripIcons(label), testStateNames[state]); export const testConfigurationGroupNames: { [K in TestRunProfileBitset]: string } = { [TestRunProfileBitset.Debug]: localize('testGroup.debug', 'Debug'), diff --git a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts index 42e68e67209..f8b3c45493a 100644 --- a/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts @@ -107,7 +107,7 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< * Applies the diff to the collection. */ public override apply(diff: TestsDiff) { - let prevBusy = this.busyControllerCount; + const prevBusy = this.busyControllerCount; super.apply(diff); if (prevBusy !== this.busyControllerCount) { diff --git a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts index 0be3c3dcc8d..bdb89d91939 100644 --- a/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts +++ b/src/vs/workbench/contrib/testing/common/testExplorerFilterState.ts @@ -91,7 +91,7 @@ export class TestExplorerFilterState implements ITestExplorerFilterState { /** @inheritdoc */ public readonly fuzzy = MutableObservableValue.stored(new StoredValue<boolean>({ key: 'testHistoryFuzzy', - scope: StorageScope.GLOBAL, + scope: StorageScope.PROFILE, target: StorageTarget.USER, }, this.storageService), false); diff --git a/src/vs/workbench/contrib/testing/common/testId.ts b/src/vs/workbench/contrib/testing/common/testId.ts index ead4e821376..a5b5323aff9 100644 --- a/src/vs/workbench/contrib/testing/common/testId.ts +++ b/src/vs/workbench/contrib/testing/common/testId.ts @@ -39,7 +39,7 @@ export class TestId { return new TestId([rootId]); } - let path = [item.id]; + const path = [item.id]; for (let i = parent; i && i.id !== rootId; i = i.parent) { path.push(i.id); } diff --git a/src/vs/workbench/contrib/testing/common/testItemCollection.ts b/src/vs/workbench/contrib/testing/common/testItemCollection.ts index 06ad3b1b4b7..9a72a25c1ac 100644 --- a/src/vs/workbench/contrib/testing/common/testItemCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testItemCollection.ts @@ -134,7 +134,7 @@ const diffTestItems = (a: ITestItem, b: ITestItem) => { return output as Partial<ITestItem> | undefined; }; -export interface ITestChildrenLike<T> extends Iterable<T> { +export interface ITestChildrenLike<T> extends Iterable<[string, T]> { get(id: string): T | undefined; delete(id: string): void; } @@ -356,7 +356,7 @@ export class TestItemCollection<T extends ITestItemLike> extends Disposable { this.connectItemAndChildren(actual, internal, parent); // Remove any orphaned children. - for (const child of oldChildren) { + for (const [_, child] of oldChildren) { if (!this.options.getChildren(actual).get(child.id)) { this.removeItem(TestId.joinToString(fullId, child.id)); } @@ -417,7 +417,7 @@ export class TestItemCollection<T extends ITestItemLike> extends Disposable { this.connectItem(actual, internal, parent); // Discover any existing children that might have already been added - for (const child of this.options.getChildren(actual)) { + for (const [_, child] of this.options.getChildren(actual)) { this.upsertItem(child, internal); } } @@ -464,7 +464,7 @@ export class TestItemCollection<T extends ITestItemLike> extends Disposable { } const expandRequests: Promise<void>[] = []; - for (const child of this.options.getChildren(internal.actual)) { + 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); @@ -544,7 +544,7 @@ export class TestItemCollection<T extends ITestItemLike> extends Disposable { } this.tree.delete(item.fullId.toString()); - for (const child of this.options.getChildren(item.actual)) { + for (const [_, child] of this.options.getChildren(item.actual)) { queue.push(this.tree.get(TestId.joinToString(item.fullId, child.id))); } } @@ -561,8 +561,8 @@ export class TestItemCollection<T extends ITestItemLike> extends Disposable { } } -/** Implementation os vscode.TestItemCollection */ -export interface ITestItemChildren<T extends ITestItemLike> extends Iterable<T> { +/** Implementation of vscode.TestItemCollection */ +export interface ITestItemChildren<T extends ITestItemLike> extends Iterable<[string, T]> { readonly size: number; replace(items: readonly T[]): void; forEach(callback: (item: T, collection: this) => unknown, thisArg?: unknown): void; @@ -608,6 +608,11 @@ export const createTestItemChildren = <T extends ITestItemLike>(api: ITestItemAp }, /** @inheritdoc */ + [Symbol.iterator](): IterableIterator<[string, T]> { + return mapped.entries(); + }, + + /** @inheritdoc */ replace(items: Iterable<T>) { const newMapped = new Map<string, T>(); const toDelete = new Set(mapped.keys()); @@ -670,10 +675,5 @@ export const createTestItemChildren = <T extends ITestItemLike>(api: ITestItemAp toJSON() { return Array.from(mapped.values()); }, - - /** @inheritdoc */ - [Symbol.iterator]() { - return mapped.values(); - }, }; }; diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index 9fd9b315d93..7f630b02f45 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -55,7 +55,7 @@ export class TestObjectTree<T> extends ObjectTree<T, any> { public getRendered(getProperty?: string) { const elements = element.querySelectorAll<HTMLElement>('.monaco-tl-contents'); const sorted = [...elements].sort((a, b) => pos(a) - pos(b)); - let chain: SerializedTree[] = [{ e: '', children: [] }]; + const chain: SerializedTree[] = [{ e: '', children: [] }]; for (const element of sorted) { const [depthStr, label] = element.textContent!.split(':'); const depth = Number(depthStr); diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 30308a2a8fb..adb89a15354 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -6,9 +6,10 @@ import { localize } from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; import { Registry } from 'vs/platform/registry/common/platform'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme, ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IColorRegistry, Extensions as ColorRegistryExtensions } from 'vs/platform/theme/common/colorRegistry'; @@ -34,6 +35,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { FileIconThemeData } from 'vs/workbench/services/themes/browser/fileIconThemeData'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const manageExtensionIcon = registerIcon('theme-selection-manage-extension', Codicon.gear, localize('manageExtensionIcon', 'Icon for the \'Manage\' action in the theme selection quick pick.')); @@ -99,16 +101,18 @@ class MarketplaceThemesPicker { try { const installedExtensions = await this._installedExtensions; - const options = { text: `${this.marketplaceQuery} ${value}`, pageSize: 40 }; + const options = { text: `${this.marketplaceQuery} ${value}`, pageSize: 20 }; const pager = await this.extensionGalleryService.query(options, token); - for (let i = 0; i < pager.total && i < 1; i++) { + for (let i = 0; i < pager.total && i < 1; i++) { // loading multiple pages is turned of for now to avoid flickering if (token.isCancellationRequested) { break; } const nThemes = this._marketplaceThemes.length; + const gallery = i === 0 ? pager.firstPage : await pager.getPage(i, token); - const gallery = await pager.getPage(i, token); + const promises: Promise<IWorkbenchTheme[]>[] = []; + const promisesGalleries = []; for (let i = 0; i < gallery.length; i++) { if (token.isCancellationRequested) { break; @@ -116,12 +120,18 @@ class MarketplaceThemesPicker { const ext = gallery[i]; if (!installedExtensions.has(ext.identifier.id) && !this._marketplaceExtensions.has(ext.identifier.id)) { this._marketplaceExtensions.add(ext.identifier.id); - const themes = await this.getMarketplaceColorThemes(ext.publisher, ext.name, ext.version); - for (const theme of themes) { - this._marketplaceThemes.push({ id: theme.id, theme: theme, label: theme.label, description: `${ext.displayName} · ${ext.publisherDisplayName}`, galleryExtension: ext, buttons: [configureButton] }); - } + promises.push(this.getMarketplaceColorThemes(ext.publisher, ext.name, ext.version)); + promisesGalleries.push(ext); + } + } + const allThemes = await Promise.all(promises); + for (let i = 0; i < allThemes.length; i++) { + const ext = promisesGalleries[i]; + for (const theme of allThemes[i]) { + this._marketplaceThemes.push({ id: theme.id, theme: theme, label: theme.label, description: `${ext.displayName} · ${ext.publisherDisplayName}`, galleryExtension: ext, buttons: [configureButton] }); } } + if (nThemes !== this._marketplaceThemes.length) { this._marketplaceThemes.sort((t1, t2) => t1.label.localeCompare(t2.label)); this._onDidChange.fire(); @@ -151,7 +161,7 @@ class MarketplaceThemesPicker { quickpick.canSelectMany = false; quickpick.onDidChangeValue(() => this.trigger(quickpick.value)); quickpick.onDidAccept(async _ => { - let themeItem = quickpick.selectedItems[0]; + const themeItem = quickpick.selectedItems[0]; if (themeItem?.galleryExtension) { result = 'selected'; quickpick.hide(); @@ -456,7 +466,10 @@ registerAction2(class extends Action2 { CommandsRegistry.registerCommand('workbench.action.previewColorTheme', async function (accessor: ServicesAccessor, extension: { publisher: string; name: string; version: string }, themeSettingsId?: string) { const themeService = accessor.get(IWorkbenchThemeService); - const themes = await themeService.getMarketplaceColorThemes(extension.publisher, extension.name, extension.version); + let themes = findBuiltInThemes(await themeService.getColorThemes(), extension); + if (themes.length === 0) { + themes = await themeService.getMarketplaceColorThemes(extension.publisher, extension.name, extension.version); + } for (const theme of themes) { if (!themeSettingsId || theme.settingsId === themeSettingsId) { await themeService.setColorTheme(theme, 'preview'); @@ -466,6 +479,10 @@ CommandsRegistry.registerCommand('workbench.action.previewColorTheme', async fun return undefined; }); +function findBuiltInThemes(themes: IWorkbenchColorTheme[], extension: { publisher: string; name: string }): IWorkbenchColorTheme[] { + return themes.filter(({ extensionData }) => extensionData && extensionData.extensionIsBuiltin && equalsIgnoreCase(extensionData.extensionPublisher, extension.publisher) && equalsIgnoreCase(extensionData.extensionName, extension.name)); +} + function configurationEntries(label: string): QuickPickInput<ThemeItem>[] { return [ { @@ -512,7 +529,7 @@ function toEntry(theme: IWorkbenchTheme): ThemeItem { function toEntries(themes: Array<IWorkbenchTheme>, label?: string): QuickPickInput<ThemeItem>[] { const sorter = (t1: ThemeItem, t2: ThemeItem) => t1.label.localeCompare(t2.label); - let entries: QuickPickInput<ThemeItem>[] = themes.map(toEntry).sort(sorter); + const entries: QuickPickInput<ThemeItem>[] = themes.map(toEntry).sort(sorter); if (entries.length > 0 && label) { entries.unshift({ type: 'separator', label }); } @@ -575,6 +592,51 @@ registerAction2(class extends Action2 { } }); +const toggleLightDarkThemesCommandId = 'workbench.action.toggleLightDarkThemes'; + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: toggleLightDarkThemesCommandId, + title: { value: localize('toggleLightDarkThemes.label', "Toggle between Light/Dark Themes"), original: 'Toggle between Light/Dark Themes' }, + category: CATEGORIES.Preferences, + f1: true, + }); + } + + override async run(accessor: ServicesAccessor) { + const themeService = accessor.get(IWorkbenchThemeService); + const configurationService = accessor.get(IConfigurationService); + + const currentTheme = themeService.getColorTheme(); + let newSettingsId: string = ThemeSettings.PREFERRED_DARK_THEME; + switch (currentTheme.type) { + case ColorScheme.LIGHT: + newSettingsId = ThemeSettings.PREFERRED_DARK_THEME; + break; + case ColorScheme.DARK: + newSettingsId = ThemeSettings.PREFERRED_LIGHT_THEME; + break; + case ColorScheme.HIGH_CONTRAST_LIGHT: + newSettingsId = ThemeSettings.PREFERRED_HC_DARK_THEME; + break; + case ColorScheme.HIGH_CONTRAST_DARK: + newSettingsId = ThemeSettings.PREFERRED_HC_LIGHT_THEME; + break; + } + + const themeSettingId: string = configurationService.getValue(newSettingsId); + + if (themeSettingId && typeof themeSettingId === 'string') { + const theme = (await themeService.getColorThemes()).find(t => t.settingsId === themeSettingId); + if (theme) { + themeService.setColorTheme(theme.id, 'auto'); + } + } + } +}); + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { group: '4_themes', command: { @@ -602,7 +664,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { order: 3 }); - MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '4_themes', command: { diff --git a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts index 2fbc546da95..8560807503e 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts @@ -12,7 +12,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ITextMateService } from 'vs/workbench/services/textMate/browser/textMate'; import type { IGrammar, StackElement } from 'vscode-textmate'; -import { TokenizationRegistry, TokenMetadata } from 'vs/editor/common/languages'; +import { TokenizationRegistry } from 'vs/editor/common/languages'; +import { TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; import { ThemeRule, findMatchingThemeRule } from 'vs/workbench/services/textMate/common/TMHelper'; import { Color } from 'vs/base/common/color'; import { IFileService } from 'vs/platform/files/common/files'; @@ -48,7 +49,7 @@ class ThemeDocument { this._cache = Object.create(null); this._defaultColor = '#000000'; for (let i = 0, len = this._theme.tokenColors.length; i < len; i++) { - let rule = this._theme.tokenColors[i]; + const rule = this._theme.tokenColors[i]; if (!rule.scope) { this._defaultColor = rule.settings.foreground!; } @@ -61,9 +62,9 @@ class ThemeDocument { public explainTokenColor(scopes: string, color: Color): string { - let matchingRule = this._findMatchingThemeRule(scopes); + const matchingRule = this._findMatchingThemeRule(scopes); if (!matchingRule) { - let expected = Color.fromHex(this._defaultColor); + const expected = Color.fromHex(this._defaultColor); // No matching rule if (!color.equals(expected)) { throw new Error(`[${this._theme.label}]: Unexpected color ${Color.Format.CSS.formatHexA(color)} for ${scopes}. Expected default ${Color.Format.CSS.formatHexA(expected)}`); @@ -71,7 +72,7 @@ class ThemeDocument { return this._generateExplanation('default', color); } - let expected = Color.fromHex(matchingRule.settings.foreground!); + const expected = Color.fromHex(matchingRule.settings.foreground!); if (!color.equals(expected)) { throw new Error(`[${this._theme.label}]: Unexpected color ${Color.Format.CSS.formatHexA(color)} for ${scopes}. Expected ${Color.Format.CSS.formatHexA(expected)} coming in from ${matchingRule.rawSelector}`); } @@ -96,21 +97,22 @@ class Snapper { } private _themedTokenize(grammar: IGrammar, lines: string[]): IThemedToken[] { - let colorMap = TokenizationRegistry.getColorMap(); + const colorMap = TokenizationRegistry.getColorMap(); let state: StackElement | null = null; - let result: IThemedToken[] = [], resultLen = 0; + const result: IThemedToken[] = []; + let resultLen = 0; for (let i = 0, len = lines.length; i < len; i++) { - let line = lines[i]; + const line = lines[i]; - let tokenizationResult = grammar.tokenizeLine2(line, state); + const tokenizationResult = grammar.tokenizeLine2(line, state); for (let j = 0, lenJ = tokenizationResult.tokens.length >>> 1; j < lenJ; j++) { - let startOffset = tokenizationResult.tokens[(j << 1)]; - let metadata = tokenizationResult.tokens[(j << 1) + 1]; - let endOffset = j + 1 < lenJ ? tokenizationResult.tokens[((j + 1) << 1)] : line.length; - let tokenText = line.substring(startOffset, endOffset); + const startOffset = tokenizationResult.tokens[(j << 1)]; + const metadata = tokenizationResult.tokens[(j << 1) + 1]; + const endOffset = j + 1 < lenJ ? tokenizationResult.tokens[((j + 1) << 1)] : line.length; + const tokenText = line.substring(startOffset, endOffset); - let color = TokenMetadata.getForeground(metadata); + const color = TokenMetadata.getForeground(metadata); result[resultLen++] = { text: tokenText, @@ -126,18 +128,18 @@ class Snapper { private _tokenize(grammar: IGrammar, lines: string[]): IToken[] { let state: StackElement | null = null; - let result: IToken[] = []; + const result: IToken[] = []; let resultLen = 0; for (let i = 0, len = lines.length; i < len; i++) { - let line = lines[i]; + const line = lines[i]; - let tokenizationResult = grammar.tokenizeLine(line, state); + const tokenizationResult = grammar.tokenizeLine(line, state); let lastScopes: string | null = null; for (let j = 0, lenJ = tokenizationResult.tokens.length; j < lenJ; j++) { - let token = tokenizationResult.tokens[j]; - let tokenText = line.substring(token.startIndex, token.endIndex); - let tokenScopes = token.scopes.join(' '); + const token = tokenizationResult.tokens[j]; + const tokenText = line.substring(token.startIndex, token.endIndex); + const tokenScopes = token.scopes.join(' '); if (lastScopes === tokenScopes) { result[resultLen - 1].c += tokenText; @@ -163,26 +165,26 @@ class Snapper { } private async _getThemesResult(grammar: IGrammar, lines: string[]): Promise<IThemesResult> { - let currentTheme = this.themeService.getColorTheme(); + const currentTheme = this.themeService.getColorTheme(); - let getThemeName = (id: string) => { - let part = 'vscode-theme-defaults-themes-'; - let startIdx = id.indexOf(part); + const getThemeName = (id: string) => { + const part = 'vscode-theme-defaults-themes-'; + const startIdx = id.indexOf(part); if (startIdx !== -1) { return id.substring(startIdx + part.length, id.length - 5); } return undefined; }; - let result: IThemesResult = {}; + const result: IThemesResult = {}; - let themeDatas = await this.themeService.getColorThemes(); - let defaultThemes = themeDatas.filter(themeData => !!getThemeName(themeData.id)); - for (let defaultTheme of defaultThemes) { - let themeId = defaultTheme.id; - let success = await this.themeService.setColorTheme(themeId, undefined); + const themeDatas = await this.themeService.getColorThemes(); + const defaultThemes = themeDatas.filter(themeData => !!getThemeName(themeData.id)); + for (const defaultTheme of defaultThemes) { + const themeId = defaultTheme.id; + const success = await this.themeService.setColorTheme(themeId, undefined); if (success) { - let themeName = getThemeName(themeId); + const themeName = getThemeName(themeId); result[themeName!] = { document: new ThemeDocument(this.themeService.getColorTheme()), tokens: this._themedTokenize(grammar, lines) @@ -194,17 +196,17 @@ class Snapper { } private _enrichResult(result: IToken[], themesResult: IThemesResult): void { - let index: { [themeName: string]: number } = {}; - let themeNames = Object.keys(themesResult); + const index: { [themeName: string]: number } = {}; + const themeNames = Object.keys(themesResult); for (const themeName of themeNames) { index[themeName] = 0; } for (let i = 0, len = result.length; i < len; i++) { - let token = result[i]; + const token = result[i]; for (const themeName of themeNames) { - let themedToken = themesResult[themeName].tokens[index[themeName]]; + const themedToken = themesResult[themeName].tokens[index[themeName]]; themedToken.text = themedToken.text.substr(token.c.length); token.r[themeName] = themesResult[themeName].document.explainTokenColor(token.t, themedToken.color); @@ -221,9 +223,9 @@ class Snapper { if (!grammar) { return []; } - let lines = splitLines(content); + const lines = splitLines(content); - let result = this._tokenize(grammar, lines); + const result = this._tokenize(grammar, lines); return this._getThemesResult(grammar, lines).then((themesResult) => { this._enrichResult(result, themesResult); return result.filter(t => t.c.length > 0); @@ -234,10 +236,10 @@ class Snapper { CommandsRegistry.registerCommand('_workbench.captureSyntaxTokens', function (accessor: ServicesAccessor, resource: URI) { - let process = (resource: URI) => { - let fileService = accessor.get(IFileService); - let fileName = basename(resource); - let snapper = accessor.get(IInstantiationService).createInstance(Snapper); + const process = (resource: URI) => { + const fileService = accessor.get(IFileService); + const fileName = basename(resource); + const snapper = accessor.get(IInstantiationService).createInstance(Snapper); return fileService.readFile(resource).then(content => { return snapper.captureSyntaxTokens(fileName, content.value.toString()); diff --git a/src/vs/workbench/contrib/themes/test/electron-browser/colorRegistry.releaseTest.ts b/src/vs/workbench/contrib/themes/test/electron-browser/colorRegistry.releaseTest.ts index 76104ae81f2..4bd0f073a2c 100644 --- a/src/vs/workbench/contrib/themes/test/electron-browser/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/contrib/themes/test/electron-browser/colorRegistry.releaseTest.ts @@ -44,7 +44,7 @@ suite('Color Registry', function () { const expression = /-\s*\`([\w\.]+)\`: (.*)/g; let m: RegExpExecArray | null; - let colorsInDoc: { [id: string]: ColorInfo } = Object.create(null); + const colorsInDoc: { [id: string]: ColorInfo } = Object.create(null); let nColorsInDoc = 0; while (m = expression.exec(content)) { colorsInDoc[m[1]] = { description: m[2], offset: m.index, length: m.length }; @@ -52,33 +52,33 @@ suite('Color Registry', function () { } assert.ok(nColorsInDoc > 0, 'theme-color.md contains to color descriptions'); - let missing = Object.create(null); - let descriptionDiffs: { [id: string]: DescriptionDiff } = Object.create(null); + const missing = Object.create(null); + const descriptionDiffs: { [id: string]: DescriptionDiff } = Object.create(null); - let themingRegistry = Registry.as<IColorRegistry>(Extensions.ColorContribution); - for (let color of themingRegistry.getColors()) { + const themingRegistry = Registry.as<IColorRegistry>(Extensions.ColorContribution); + for (const color of themingRegistry.getColors()) { if (!colorsInDoc[color.id]) { if (!color.deprecationMessage) { missing[color.id] = getDescription(color); } } else { - let docDescription = colorsInDoc[color.id].description; - let specDescription = getDescription(color); + const docDescription = colorsInDoc[color.id].description; + const specDescription = getDescription(color); if (docDescription !== specDescription) { descriptionDiffs[color.id] = { docDescription, specDescription }; } delete colorsInDoc[color.id]; } } - let colorsInExtensions = await getColorsFromExtension(); - for (let colorId in colorsInExtensions) { + const colorsInExtensions = await getColorsFromExtension(); + for (const colorId in colorsInExtensions) { if (!colorsInDoc[colorId]) { missing[colorId] = colorsInExtensions[colorId]; } else { delete colorsInDoc[colorId]; } } - for (let colorId of experimental) { + for (const colorId of experimental) { if (missing[colorId]) { delete missing[colorId]; } @@ -87,10 +87,10 @@ suite('Color Registry', function () { } } - let undocumentedKeys = Object.keys(missing).map(k => `\`${k}\`: ${missing[k]}`); + const undocumentedKeys = Object.keys(missing).map(k => `\`${k}\`: ${missing[k]}`); assert.deepStrictEqual(undocumentedKeys, [], 'Undocumented colors ids'); - let superfluousKeys = Object.keys(colorsInDoc); + const superfluousKeys = Object.keys(colorsInDoc); assert.deepStrictEqual(superfluousKeys, [], 'Colors ids in doc that do not exist'); }); @@ -105,18 +105,18 @@ function getDescription(color: ColorContribution) { } async function getColorsFromExtension(): Promise<{ [id: string]: string }> { - let extPath = getPathFromAmdModule(require, '../../../../../../../extensions'); - let extFolders = await pfs.Promises.readDirsInDir(extPath); - let result: { [id: string]: string } = Object.create(null); - for (let folder of extFolders) { + const extPath = getPathFromAmdModule(require, '../../../../../../../extensions'); + const extFolders = await pfs.Promises.readDirsInDir(extPath); + const result: { [id: string]: string } = Object.create(null); + for (const folder of extFolders) { try { - let packageJSON = JSON.parse((await pfs.Promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); - let contributes = packageJSON['contributes']; + const packageJSON = JSON.parse((await pfs.Promises.readFile(path.join(extPath, folder, 'package.json'))).toString()); + const contributes = packageJSON['contributes']; if (contributes) { - let colors = contributes['colors']; + const colors = contributes['colors']; if (colors) { - for (let color of colors) { - let colorId = color['id']; + for (const color of colors) { + const colorId = color['id']; if (colorId) { result[colorId] = colorId['description']; } diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts index ca9c2547965..eb2b7e6a474 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts @@ -89,7 +89,7 @@ class TypeHierarchyController implements IEditorContribution { const cts = new CancellationTokenSource(); const model = TypeHierarchyModel.create(document, position, cts.token); - const direction = sanitizedDirection(this._storageService.get(TypeHierarchyController._storageDirectionKey, StorageScope.GLOBAL, TypeHierarchyDirection.Subtypes)); + const direction = sanitizedDirection(this._storageService.get(TypeHierarchyController._storageDirectionKey, StorageScope.PROFILE, TypeHierarchyDirection.Subtypes)); this._showTypeHierarchyWidget(position, direction, model, cts); } @@ -103,7 +103,7 @@ class TypeHierarchyController implements IEditorContribution { this._widget.showLoading(); this._sessionDisposables.add(this._widget.onDidClose(() => { this.endTypeHierarchy(); - this._storageService.store(TypeHierarchyController._storageDirectionKey, this._widget!.direction, StorageScope.GLOBAL, StorageTarget.USER); + this._storageService.store(TypeHierarchyController._storageDirectionKey, this._widget!.direction, StorageScope.PROFILE, StorageTarget.USER); })); this._sessionDisposables.add({ dispose() { cts.dispose(true); } }); this._sessionDisposables.add(this._widget); diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts index d0956de5457..5fa49e2ea7d 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts @@ -45,11 +45,11 @@ const enum State { class LayoutInfo { static store(info: LayoutInfo, storageService: IStorageService): void { - storageService.store('typeHierarchyPeekLayout', JSON.stringify(info), StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store('typeHierarchyPeekLayout', JSON.stringify(info), StorageScope.PROFILE, StorageTarget.MACHINE); } static retrieve(storageService: IStorageService): LayoutInfo { - const value = storageService.get('typeHierarchyPeekLayout', StorageScope.GLOBAL, '{}'); + const value = storageService.get('typeHierarchyPeekLayout', StorageScope.PROFILE, '{}'); const defaultInfo: LayoutInfo = { ratio: 0.7, height: 17 }; try { return { ...defaultInfo, ...JSON.parse(value) }; @@ -164,7 +164,7 @@ export class TypeHierarchyTreePeekWidget extends peekView.PeekViewWidget { const editorContainer = document.createElement('div'); editorContainer.classList.add('editor'); container.appendChild(editorContainer); - let editorOptions: IEditorOptions = { + const editorOptions: IEditorOptions = { scrollBeyondLastLine: false, scrollbar: { verticalScrollbarSize: 14, @@ -320,7 +320,7 @@ export class TypeHierarchyTreePeekWidget extends peekView.PeekViewWidget { this._editor.setModel(value.object.textEditorModel); // set decorations for type ranges - let decorations: IModelDeltaDecoration[] = []; + const decorations: IModelDeltaDecoration[] = []; let fullRange: IRange | undefined; const loc = { uri: element.item.uri, range: element.item.selectionRange }; if (loc.uri.toString() === previewUri.toString()) { diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree.ts index 6a1e3e1466f..f03ecfaba85 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyTree.ts @@ -106,7 +106,7 @@ export class TypeRenderer implements ITreeRenderer<Type, FuzzyScore, TypeRenderi renderTemplate(container: HTMLElement): TypeRenderingTemplate { container.classList.add('typehierarchy-element'); - let icon = document.createElement('div'); + const icon = document.createElement('div'); container.appendChild(icon); const label = new IconLabel(container, { supportHighlights: true }); return new TypeRenderingTemplate(icon, label); diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 052ec7ea538..1ff40d85b54 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -163,7 +163,7 @@ export class ProductContribution implements IWorkbenchContribution { return; } - const lastVersion = parseVersion(storageService.get(ProductContribution.KEY, StorageScope.GLOBAL, '')); + const lastVersion = parseVersion(storageService.get(ProductContribution.KEY, StorageScope.APPLICATION, '')); const currentVersion = parseVersion(productService.version); const shouldShowReleaseNotes = configurationService.getValue<boolean>('update.showReleaseNotes'); const releaseNotesUrl = productService.releaseNotesUrl; @@ -186,7 +186,7 @@ export class ProductContribution implements IWorkbenchContribution { }); } - storageService.store(ProductContribution.KEY, productService.version, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(ProductContribution.KEY, productService.version, StorageScope.APPLICATION, StorageTarget.MACHINE); }); } } @@ -224,12 +224,12 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu */ const currentVersion = this.productService.commit; - const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); + const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.APPLICATION); // if current version != stored version, clear both fields if (currentVersion !== lastKnownVersion) { - this.storageService.remove('update/lastKnownVersion', StorageScope.GLOBAL); - this.storageService.remove('update/updateNotificationTime', StorageScope.GLOBAL); + this.storageService.remove('update/lastKnownVersion', StorageScope.APPLICATION); + this.storageService.remove('update/updateNotificationTime', StorageScope.APPLICATION); } this.registerGlobalActivityActions(); @@ -400,15 +400,15 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu private shouldShowNotification(): boolean { const currentVersion = this.productService.commit; const currentMillis = new Date().getTime(); - const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); + const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.APPLICATION); // if version != stored version, save version and date if (currentVersion !== lastKnownVersion) { - this.storageService.store('update/lastKnownVersion', currentVersion!, StorageScope.GLOBAL, StorageTarget.MACHINE); - this.storageService.store('update/updateNotificationTime', currentMillis, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store('update/lastKnownVersion', currentVersion!, StorageScope.APPLICATION, StorageTarget.MACHINE); + this.storageService.store('update/updateNotificationTime', currentMillis, StorageScope.APPLICATION, StorageTarget.MACHINE); } - const updateNotificationMillis = this.storageService.getNumber('update/updateNotificationTime', StorageScope.GLOBAL, currentMillis); + const updateNotificationMillis = this.storageService.getNumber('update/updateNotificationTime', StorageScope.APPLICATION, currentMillis); const diffDays = (currentMillis - updateNotificationMillis) / (1000 * 60 * 60 * 24); return diffDays > 5; @@ -536,12 +536,12 @@ export class SwitchProductQualityContribution extends Disposable implements IWor const userDataSyncStore = userDataSyncStoreManagementService.userDataSyncStore; let userDataSyncStoreType: UserDataSyncStoreType | undefined; if (userDataSyncStore && isSwitchingToInsiders && userDataSyncEnablementService.isEnabled() - && !storageService.getBoolean(selectSettingsSyncServiceDialogShownKey, StorageScope.GLOBAL, false)) { + && !storageService.getBoolean(selectSettingsSyncServiceDialogShownKey, StorageScope.APPLICATION, false)) { userDataSyncStoreType = await this.selectSettingsSyncService(dialogService); if (!userDataSyncStoreType) { return; } - storageService.store(selectSettingsSyncServiceDialogShownKey, true, StorageScope.GLOBAL, StorageTarget.USER); + storageService.store(selectSettingsSyncServiceDialogShownKey, true, StorageScope.APPLICATION, StorageTarget.USER); if (userDataSyncStoreType === 'stable') { // Update the stable service type in the current window, so that it uses stable service after switched to insiders version (after reload). await userDataSyncStoreManagementService.switch(userDataSyncStoreType); @@ -576,7 +576,7 @@ export class SwitchProductQualityContribution extends Disposable implements IWor } else { // Reset if (userDataSyncStoreType) { - storageService.remove(selectSettingsSyncServiceDialogShownKey, StorageScope.GLOBAL); + storageService.remove(selectSettingsSyncServiceDialogShownKey, StorageScope.APPLICATION); } } } catch (error) { diff --git a/src/vs/workbench/contrib/url/browser/trustedDomains.ts b/src/vs/workbench/contrib/url/browser/trustedDomains.ts index 613047e7a97..2019b04e521 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomains.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomains.ts @@ -112,11 +112,11 @@ export async function configureOpenerTrustedDomainsHandler( case 'trust': { const itemToTrust = pickedResult.toTrust; if (trustedDomains.indexOf(itemToTrust) === -1) { - storageService.remove(TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, StorageScope.GLOBAL); + storageService.remove(TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, StorageScope.APPLICATION); storageService.store( TRUSTED_DOMAINS_STORAGE_KEY, JSON.stringify([...trustedDomains, itemToTrust]), - StorageScope.GLOBAL, + StorageScope.APPLICATION, StorageTarget.USER ); @@ -214,7 +214,7 @@ export function readStaticTrustedDomains(accessor: ServicesAccessor): IStaticTru let trustedDomains: string[] = []; try { - const trustedDomainsSrc = storageService.get(TRUSTED_DOMAINS_STORAGE_KEY, StorageScope.GLOBAL); + const trustedDomainsSrc = storageService.get(TRUSTED_DOMAINS_STORAGE_KEY, StorageScope.APPLICATION); if (trustedDomainsSrc) { trustedDomains = JSON.parse(trustedDomainsSrc); } diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsFileSystemProvider.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsFileSystemProvider.ts index 675de94d299..c13d93e6721 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsFileSystemProvider.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsFileSystemProvider.ts @@ -109,7 +109,7 @@ export class TrustedDomainsFileSystemProvider implements IFileSystemProviderWith async readFile(resource: URI): Promise<Uint8Array> { let trustedDomainsContent = this.storageService.get( TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, - StorageScope.GLOBAL + StorageScope.APPLICATION ); const configuring: string | undefined = resource.fragment; @@ -134,11 +134,11 @@ export class TrustedDomainsFileSystemProvider implements IFileSystemProviderWith const trustedDomainsContent = VSBuffer.wrap(content).toString(); const trustedDomains = parse(trustedDomainsContent); - this.storageService.store(TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, trustedDomainsContent, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(TRUSTED_DOMAINS_CONTENT_STORAGE_KEY, trustedDomainsContent, StorageScope.APPLICATION, StorageTarget.USER); this.storageService.store( TRUSTED_DOMAINS_STORAGE_KEY, JSON.stringify(trustedDomains) || '', - StorageScope.GLOBAL, + StorageScope.APPLICATION, StorageTarget.USER ); } catch (err) { } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.contribution.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.contribution.ts new file mode 100644 index 00000000000..ae8e9bbaf5d --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.contribution.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; +import { UserDataProfilesWorkbenchContribution } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfile'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import '../common/userDataProfileActions'; + +const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(UserDataProfilesWorkbenchContribution, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts new file mode 100644 index 00000000000..8a75af540d3 --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { isWeb } from 'vs/base/common/platform'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize } from 'vs/nls'; +import { Action2, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { IUserDataProfile, IUserDataProfilesService, PROFILES_ENABLEMENT_CONFIG } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { IUserDataProfileManagementService, IUserDataProfileService, ManageProfilesSubMenu, PROFILES_CATEGORY, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +const CONTEXT_CURRENT_PROFILE = new RawContextKey<string>('currentUserDataProfile', ''); + +export class UserDataProfilesWorkbenchContribution extends Disposable implements IWorkbenchContribution { + + private readonly currentProfileContext: IContextKey<string>; + + constructor( + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IStatusbarService private readonly statusBarService: IStatusbarService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IProductService private readonly productService: IProductService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(); + + this.registerConfiguration(); + + this.currentProfileContext = CONTEXT_CURRENT_PROFILE.bindTo(contextKeyService); + this.currentProfileContext.set(this.userDataProfileService.currentProfile.id); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => this.currentProfileContext.set(this.userDataProfileService.currentProfile.id))); + + this.updateStatus(); + this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.userDataProfileService.onDidChangeCurrentProfile, this.userDataProfilesService.onDidChangeProfiles)(() => this.updateStatus())); + + this.registerActions(); + } + + private registerConfiguration(): void { + if (!isWeb && this.productService.quality !== 'stable') { + Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({ + ...workbenchConfigurationNodeBase, + 'properties': { + [PROFILES_ENABLEMENT_CONFIG]: { + 'type': 'boolean', + 'default': false, + 'description': localize('workbench.experimental.settingsProfiles.enabled', "Controls whether to enable the Settings Profiles preview feature."), + scope: ConfigurationScope.APPLICATION + } + } + }); + } + } + + private registerActions(): void { + this.registerManageProfilesSubMenu(); + + this.registerProfilesActions(); + this._register(this.userDataProfilesService.onDidChangeProfiles(() => this.registerProfilesActions())); + } + + private registerManageProfilesSubMenu(): void { + const that = this; + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, <ISubmenuItem>{ + get title() { return localize('manageProfiles', "{0} ({1})", PROFILES_TTILE.value, that.userDataProfileService.currentProfile.name); }, + submenu: ManageProfilesSubMenu, + group: '5_profiles', + when: PROFILES_ENABLEMENT_CONTEXT, + order: 3 + }); + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, <ISubmenuItem>{ + title: PROFILES_TTILE, + submenu: ManageProfilesSubMenu, + group: '5_profiles', + when: PROFILES_ENABLEMENT_CONTEXT, + order: 3 + }); + MenuRegistry.appendMenuItem(MenuId.AccountsContext, <ISubmenuItem>{ + get title() { return localize('manageProfiles', "{0} ({1})", PROFILES_TTILE.value, that.userDataProfileService.currentProfile.name); }, + submenu: ManageProfilesSubMenu, + group: '1_profiles', + when: PROFILES_ENABLEMENT_CONTEXT, + }); + } + + private readonly profilesDisposable = this._register(new MutableDisposable<DisposableStore>()); + private registerProfilesActions(): void { + this.profilesDisposable.value = new DisposableStore(); + for (const profile of this.userDataProfilesService.profiles) { + this.profilesDisposable.value.add(this.registerProfileEntryAction(profile)); + } + } + + private registerProfileEntryAction(profile: IUserDataProfile): IDisposable { + const that = this; + return registerAction2(class ProfileEntryAction extends Action2 { + constructor() { + super({ + id: `workbench.profiles.actions.profileEntry.${profile.id}`, + title: profile.name, + toggled: ContextKeyExpr.equals(CONTEXT_CURRENT_PROFILE.key, profile.id), + precondition: ContextKeyExpr.notEquals(CONTEXT_CURRENT_PROFILE.key, profile.id), + menu: [ + { + id: ManageProfilesSubMenu, + group: '0_profiles', + when: PROFILES_ENABLEMENT_CONTEXT, + } + ] + }); + } + async run(accessor: ServicesAccessor) { + return that.userDataProfileManagementService.switchProfile(profile); + } + }); + } + + private profileStatusAccessor: IStatusbarEntryAccessor | undefined; + private updateStatus(): void { + if (this.userDataProfilesService.profiles.length) { + const statusBarEntry: IStatusbarEntry = { + name: PROFILES_CATEGORY, + command: 'workbench.profiles.actions.switchProfile', + ariaLabel: localize('currentProfile', "Current Settings Profile is {0}", this.userDataProfileService.currentProfile.name), + text: `$(${Codicon.multipleWindows.id}) ${this.userDataProfileService.currentProfile.name!}`, + tooltip: localize('profileTooltip', "{0}: {1}", PROFILES_CATEGORY, this.userDataProfileService.currentProfile.name), + color: themeColorFromId(STATUS_BAR_SETTINGS_PROFILE_FOREGROUND), + backgroundColor: themeColorFromId(STATUS_BAR_SETTINGS_PROFILE_BACKGROUND) + }; + if (this.profileStatusAccessor) { + this.profileStatusAccessor.update(statusBarEntry); + } else { + this.profileStatusAccessor = this.statusBarService.addEntry(statusBarEntry, 'status.userDataProfile', StatusbarAlignment.LEFT, Number.MAX_VALUE - 1); + } + } else { + if (this.profileStatusAccessor) { + this.profileStatusAccessor.dispose(); + this.profileStatusAccessor = undefined; + } + } + } +} + +const STATUS_BAR_SETTINGS_PROFILE_FOREGROUND = registerColor('statusBarItem.settingsProfilesForeground', { + dark: null, + light: null, + hcDark: null, + hcLight: null +}, localize('statusBarItemSettingsProfileForeground', "Foreground color for the settings profile entry on the status bar.")); + +const STATUS_BAR_SETTINGS_PROFILE_BACKGROUND = registerColor('statusBarItem.settingsProfilesBackground', { + dark: null, + light: null, + hcDark: null, + hcLight: null +}, localize('statusBarItemSettingsProfileBackground', "Background color for the settings profile entry on the status bar.")); diff --git a/src/vs/workbench/contrib/userDataProfile/common/userDataProfileActions.ts b/src/vs/workbench/contrib/userDataProfile/common/userDataProfileActions.ts new file mode 100644 index 00000000000..e796f1f897e --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/common/userDataProfileActions.ts @@ -0,0 +1,316 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { joinPath } from 'vs/base/common/resources'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; +import { IUserDataProfileTemplate, isUserDataProfileTemplate, IUserDataProfileManagementService, IUserDataProfileImportExportService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER, ManageProfilesSubMenu, IUserDataProfileService, PROFILES_ENABLEMENT_CONTEXT } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { CATEGORIES } from 'vs/workbench/common/actions'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; + +registerAction2(class CreateFromCurrentProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.createFromCurrentProfile', + title: { + value: localize('save profile as', "Create from Current Settings Profile..."), + original: 'Create from Current Profile...' + }, + category: PROFILES_CATEGORY, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT, + menu: [ + { + id: ManageProfilesSubMenu, + group: '1_create_profiles', + when: PROFILES_ENABLEMENT_CONTEXT, + order: 1 + } + ] + }); + } + + async run(accessor: ServicesAccessor) { + const quickInputService = accessor.get(IQuickInputService); + const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); + const name = await quickInputService.input({ + placeHolder: localize('name', "Profile name"), + title: localize('save profile as', "Create from Current Settings Profile..."), + }); + if (name) { + await userDataProfileManagementService.createAndEnterProfile(name, undefined, true); + } + } +}); + +registerAction2(class CreateEmptyProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.createProfile', + title: { + value: localize('create profile', "Create an Empty Settings Profile..."), + original: 'Create an Empty Profile...' + }, + category: PROFILES_CATEGORY, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT, + menu: [ + { + id: ManageProfilesSubMenu, + group: '1_create_profiles', + when: PROFILES_ENABLEMENT_CONTEXT, + order: 2 + } + ] + }); + } + + async run(accessor: ServicesAccessor) { + const quickInputService = accessor.get(IQuickInputService); + const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); + const name = await quickInputService.input({ + placeHolder: localize('name', "Profile name"), + title: localize('create and enter empty profile', "Create an Empty Profile..."), + }); + if (name) { + await userDataProfileManagementService.createAndEnterProfile(name); + } + } +}); + +registerAction2(class RemoveProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.removeProfile', + title: { + value: localize('remove profile', "Remove Settings Profile..."), + original: 'Remove Profile...' + }, + category: PROFILES_CATEGORY, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT, + menu: [ + { + id: ManageProfilesSubMenu, + group: '2_manage_profiles', + when: PROFILES_ENABLEMENT_CONTEXT + } + ] + }); + } + + async run(accessor: ServicesAccessor) { + const quickInputService = accessor.get(IQuickInputService); + const userDataProfileService = accessor.get(IUserDataProfileService); + const userDataProfilesService = accessor.get(IUserDataProfilesService); + const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); + + const profiles = userDataProfilesService.profiles.filter(p => p.id !== userDataProfileService.currentProfile.id && !p.isDefault); + if (profiles.length) { + const pick = await quickInputService.pick(profiles.map(profile => ({ label: profile.name, profile })), { placeHolder: localize('pick profile', "Select Settings Profile") }); + if (pick) { + await userDataProfileManagementService.removeProfile(pick.profile); + } + } + } +}); + +registerAction2(class SwitchProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.switchProfile', + title: { + value: localize('switch profile', "Switch Settings Profile..."), + original: 'Switch Settings Profile...' + }, + category: PROFILES_CATEGORY, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT, + }); + } + + async run(accessor: ServicesAccessor) { + const quickInputService = accessor.get(IQuickInputService); + const userDataProfileService = accessor.get(IUserDataProfileService); + const userDataProfilesService = accessor.get(IUserDataProfilesService); + const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); + + const profiles = userDataProfilesService.profiles; + if (profiles.length) { + const picks: Array<IQuickPickItem & { profile: IUserDataProfile }> = profiles.map(profile => ({ + label: profile.name!, + description: profile.name === userDataProfileService.currentProfile.name ? localize('current', "Current") : undefined, + profile + })); + const pick = await quickInputService.pick(picks, { placeHolder: localize('pick profile', "Select Settings Profile") }); + if (pick) { + await userDataProfileManagementService.switchProfile(pick.profile); + } + } + } +}); + +registerAction2(class CleanupProfilesAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.cleanupProfiles', + title: { + value: localize('cleanup profile', "Cleanup Settings Profiles"), + original: 'Cleanup Profiles' + }, + category: CATEGORIES.Developer, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT, + }); + } + + async run(accessor: ServicesAccessor) { + const userDataProfilesService = accessor.get(IUserDataProfilesService); + const fileService = accessor.get(IFileService); + const uriIdentityService = accessor.get(IUriIdentityService); + + const stat = await fileService.resolve(userDataProfilesService.profilesHome); + await Promise.all((stat.children || [])?.filter(child => child.isDirectory && userDataProfilesService.profiles.every(p => !uriIdentityService.extUri.isEqual(p.location, child.resource))) + .map(child => fileService.del(child.resource, { recursive: true }))); + } +}); + +registerAction2(class ExportProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.exportProfile', + title: { + value: localize('export profile', "Export Settings Profile..."), + original: 'Export Settings Profile...' + }, + category: PROFILES_CATEGORY, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT, + menu: [ + { + id: ManageProfilesSubMenu, + group: '3_import_export_profiles', + when: PROFILES_ENABLEMENT_CONTEXT, + order: 1 + } + ] + }); + } + + async run(accessor: ServicesAccessor) { + const textFileService = accessor.get(ITextFileService); + const fileDialogService = accessor.get(IFileDialogService); + const userDataProfileImportExportService = accessor.get(IUserDataProfileImportExportService); + const notificationService = accessor.get(INotificationService); + + const profileLocation = await fileDialogService.showSaveDialog({ + title: localize('export profile dialog', "Save Profile"), + filters: PROFILE_FILTER, + defaultUri: joinPath(await fileDialogService.defaultFilePath(), `profile.${PROFILE_EXTENSION}`), + }); + + if (!profileLocation) { + return; + } + + const profile = await userDataProfileImportExportService.exportProfile({ skipComments: true }); + await textFileService.create([{ resource: profileLocation, value: JSON.stringify(profile), options: { overwrite: true } }]); + + notificationService.info(localize('export success', "{0}: Exported successfully.", PROFILES_CATEGORY)); + } +}); + +registerAction2(class ImportProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.importProfile', + title: { + value: localize('import profile', "Import Settings Profile..."), + original: 'Import Settings Profile...' + }, + category: PROFILES_CATEGORY, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT, + menu: [ + { + id: ManageProfilesSubMenu, + group: '3_import_export_profiles', + when: PROFILES_ENABLEMENT_CONTEXT, + order: 2 + } + ] + }); + } + + async run(accessor: ServicesAccessor) { + const fileDialogService = accessor.get(IFileDialogService); + const quickInputService = accessor.get(IQuickInputService); + const fileService = accessor.get(IFileService); + const requestService = accessor.get(IRequestService); + const userDataProfileImportExportService = accessor.get(IUserDataProfileImportExportService); + + const disposables = new DisposableStore(); + const quickPick = disposables.add(quickInputService.createQuickPick()); + const updateQuickPickItems = (value?: string) => { + const selectFromFileItem: IQuickPickItem = { label: localize('select from file', "Import from profile file") }; + quickPick.items = value ? [{ label: localize('select from url', "Import from URL"), description: quickPick.value }, selectFromFileItem] : [selectFromFileItem]; + }; + quickPick.title = localize('import profile quick pick title', "Import Settings from a Profile"); + quickPick.placeholder = localize('import profile placeholder', "Provide profile URL or select profile file to import"); + quickPick.ignoreFocusOut = true; + disposables.add(quickPick.onDidChangeValue(updateQuickPickItems)); + updateQuickPickItems(); + quickPick.matchOnLabel = false; + quickPick.matchOnDescription = false; + disposables.add(quickPick.onDidAccept(async () => { + quickPick.hide(); + const profile = quickPick.selectedItems[0].description ? await this.getProfileFromURL(quickPick.value, requestService) : await this.getProfileFromFileSystem(fileDialogService, fileService); + if (profile) { + await userDataProfileImportExportService.importProfile(profile); + } + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + } + + private async getProfileFromFileSystem(fileDialogService: IFileDialogService, fileService: IFileService): Promise<IUserDataProfileTemplate | null> { + const profileLocation = await fileDialogService.showOpenDialog({ + canSelectFolders: false, + canSelectFiles: true, + canSelectMany: false, + filters: PROFILE_FILTER, + title: localize('import profile dialog', "Import Profile"), + }); + if (!profileLocation) { + return null; + } + const content = (await fileService.readFile(profileLocation[0])).value.toString(); + const parsed = JSON.parse(content); + return isUserDataProfileTemplate(parsed) ? parsed : null; + } + + private async getProfileFromURL(url: string, requestService: IRequestService): Promise<IUserDataProfileTemplate | null> { + const options = { type: 'GET', url }; + const context = await requestService.request(options, CancellationToken.None); + if (context.res.statusCode === 200) { + const result = await asJson(context); + return isUserDataProfileTemplate(result) ? result : null; + } else { + const message = await asText(context); + throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`); + } + } + +}); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 4a7fe8b6202..9f113cfabab 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -6,12 +6,10 @@ import { Action } from 'vs/base/common/actions'; import { getErrorMessage, isCancellationError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { isEqual, basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import type { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import type { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -20,7 +18,7 @@ import { localize } from 'vs/nls'; import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyEqualsExpr, ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -31,7 +29,6 @@ import { SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, getSyncResourceFromLocalPreview, IResourcePreview, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; -import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -55,19 +52,24 @@ import { UserDataSyncDataViews } from 'vs/workbench/contrib/userDataSync/browser import { IUserDataSyncWorkbenchService, getSyncAreaLabel, AccountStatus, CONTEXT_SYNC_STATE, CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE, CONFIGURE_SYNC_COMMAND_ID, SHOW_SYNC_LOG_COMMAND_ID, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE, SYNC_VIEW_ICON } from 'vs/workbench/services/userDataSync/common/userDataSync'; import { Codicon } from 'vs/base/common/codicons'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { EditorResolution } from 'vs/platform/editor/common/editor'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ctxIsMergeEditor } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; const CONTEXT_CONFLICTS_SOURCES = new RawContextKey<string>('conflictsSources', ''); type ConfigureSyncQuickPickItem = { id: SyncResource; label: string; description?: string }; type SyncConflictsClassification = { - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - action?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + owner: 'sandy081'; + comment: 'Response information when conflict happens during settings sync'; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'settings sync resource. eg., settings, keybindings...' }; + action?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'action taken while resolving conflicts. Eg: acceptLocal, acceptRemote' }; }; const turnOnSyncCommand = { id: 'workbench.userDataSync.actions.turnOn', title: localize('turn on sync with category', "{0}: Turn On...", SYNC_TITLE) }; @@ -113,6 +115,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IActivityService private readonly activityService: IActivityService, @INotificationService private readonly notificationService: INotificationService, @IEditorService private readonly editorService: IEditorService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IDialogService private readonly dialogService: IDialogService, @IQuickInputService private readonly quickInputService: IQuickInputService, @@ -163,7 +166,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerViews(); textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider)); - registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution); this._register(Event.any(userDataSyncService.onDidChangeStatus, userDataSyncEnablementService.onDidChangeEnablement) (() => this.turningOnSync = !userDataSyncEnablementService.isEnabled() && userDataSyncService.status !== SyncStatus.Idle)); @@ -184,7 +186,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (requiresInitialization && !this.userDataSyncEnablementService.isEnabled()) { this.updateSyncAfterInitializationContext(true); } else { - this.updateSyncAfterInitializationContext(this.storageService.getBoolean(CONTEXT_SYNC_AFTER_INITIALIZATION.key, StorageScope.GLOBAL, false)); + this.updateSyncAfterInitializationContext(this.storageService.getBoolean(CONTEXT_SYNC_AFTER_INITIALIZATION.key, StorageScope.APPLICATION, false)); } const disposable = this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => { if (this.userDataSyncEnablementService.isEnabled()) { @@ -195,7 +197,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async updateSyncAfterInitializationContext(value: boolean): Promise<void> { - this.storageService.store(CONTEXT_SYNC_AFTER_INITIALIZATION.key, value, StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(CONTEXT_SYNC_AFTER_INITIALIZATION.key, value, StorageScope.APPLICATION, StorageTarget.MACHINE); this.syncAfterInitializationContext.set(value); this.updateGlobalActivityBadge(); } @@ -436,7 +438,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (!this.hostService.hasFocus) { return; } - const resource = source === SyncResource.Settings ? this.environmentService.settingsResource : this.environmentService.keybindingsResource; + const resource = source === SyncResource.Settings ? this.userDataProfilesService.defaultProfile.settingsResource : this.userDataProfilesService.defaultProfile.keybindingsResource; if (isEqual(resource, EditorResourceAccessor.getCanonicalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }))) { // Do not show notification if the file in error is active return; @@ -725,20 +727,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async handleConflicts([syncResource, conflicts]: [SyncResource, IResourcePreview[]]): Promise<void> { for (const conflict of conflicts) { - const leftResourceName = localize({ key: 'leftResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(conflict.remoteResource)); - const rightResourceName = localize('merges', "{0} (Merges)", basename(conflict.previewResource)); - await this.editorService.openEditor({ - original: { resource: conflict.remoteResource }, - modified: { resource: conflict.previewResource }, - label: localize('sideBySideLabels', "{0} ↔ {1}", leftResourceName, rightResourceName), - description: localize('sideBySideDescription', "Settings Sync"), - options: { - preserveFocus: false, - pinned: true, - revealIfVisible: true, - override: EditorResolution.DISABLED - }, - }); + const remoteResourceName = localize({ key: 'remoteResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(conflict.remoteResource)); + const localResourceName = localize('localResourceName', "{0} (Local)", basename(conflict.remoteResource)); + const input = this.instantiationService.createInstance( + MergeEditorInput, + conflict.baseResource, + { title: localize('Yours', 'Yours'), description: localResourceName, detail: undefined, uri: conflict.localResource }, + { title: localize('Theirs', 'Theirs'), description: remoteResourceName, detail: undefined, uri: conflict.remoteResource }, + conflict.previewResource, + ); + await this.editorService.openEditor(input); } } @@ -809,6 +807,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerHelpAction(); this.registerShowLogAction(); this.registerResetSyncDataAction(); + this.registerAcceptMergesAction(); } private registerTurnOnSyncAction(): void { @@ -1283,6 +1282,37 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } + private registerAcceptMergesAction(): void { + const that = this; + this._register(registerAction2(class AcceptMergesAction extends Action2 { + constructor() { + super({ + id: 'workbench.userDataSync.actions.acceptMerges', + title: localize('accept merges title', "Accept Merge"), + menu: [{ + id: MenuId.MergeToolbar, + when: ContextKeyExpr.and(ctxIsMergeEditor, ContextKeyEqualsExpr.create('baseResourceScheme', USER_DATA_SYNC_SCHEME)), + }], + }); + } + + async run(accessor: ServicesAccessor, previewResource: URI): Promise<void> { + const textFileService = accessor.get(ITextFileService); + await textFileService.save(previewResource); + const content = await textFileService.read(previewResource); + await that.userDataSyncService.accept(this.getSyncResource(previewResource), previewResource, content.value, true); + } + + private getSyncResource(previewResource: URI): SyncResource { + const conflict = that.userDataSyncService.conflicts.find(([, conflicts]) => conflicts.some(conflict => isEqual(conflict.previewResource, previewResource))); + if (conflict) { + return conflict[0]; + } + throw new Error(`Unknown resource: ${previewResource.toString()}`); + } + })); + } + private registerViews(): void { const container = this.registerViewContainer(); this.registerDataViews(container); @@ -1342,131 +1372,3 @@ class UserDataRemoteContentProvider implements ITextModelContentProvider { return null; } } - -class AcceptChangesContribution extends Disposable implements IEditorContribution { - - static get(editor: ICodeEditor): AcceptChangesContribution | null { - return editor.getContribution<AcceptChangesContribution>(AcceptChangesContribution.ID); - } - - public static readonly ID = 'editor.contrib.acceptChangesButton'; - - private acceptChangesButton: FloatingClickWidget | undefined; - - constructor( - private editor: ICodeEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, - @INotificationService private readonly notificationService: INotificationService, - @IDialogService private readonly dialogService: IDialogService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, - ) { - super(); - - this.update(); - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.editor.onDidChangeModel(() => this.update())); - this._register(this.userDataSyncService.onDidChangeConflicts(() => this.update())); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('diffEditor.renderSideBySide'))(() => this.update())); - } - - private update(): void { - if (!this.shouldShowButton(this.editor)) { - this.disposeAcceptChangesWidgetRenderer(); - return; - } - - this.createAcceptChangesWidgetRenderer(); - } - - private shouldShowButton(editor: ICodeEditor): boolean { - const model = editor.getModel(); - if (!model) { - return false; // we need a model - } - - if (!this.userDataSyncEnablementService.isEnabled()) { - return false; - } - - const syncResourceConflicts = this.getSyncResourceConflicts(model.uri); - if (!syncResourceConflicts) { - return false; - } - - if (syncResourceConflicts[1].some(({ previewResource }) => isEqual(previewResource, model.uri))) { - return true; - } - - if (syncResourceConflicts[1].some(({ remoteResource }) => isEqual(remoteResource, model.uri))) { - return this.configurationService.getValue('diffEditor.renderSideBySide'); - } - - return false; - } - - private createAcceptChangesWidgetRenderer(): void { - if (!this.acceptChangesButton) { - const resource = this.editor.getModel()!.uri; - const [syncResource, conflicts] = this.getSyncResourceConflicts(resource)!; - const isRemote = conflicts.some(({ remoteResource }) => isEqual(remoteResource, resource)); - const acceptRemoteLabel = localize('accept remote', "Accept Remote"); - const acceptMergesLabel = localize('accept merges', "Accept Merges"); - const acceptRemoteButtonLabel = localize('accept remote button', "Accept &&Remote"); - const acceptMergesButtonLabel = localize('accept merges button', "Accept &&Merges"); - this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptMergesLabel, null); - this._register(this.acceptChangesButton.onClick(async () => { - const model = this.editor.getModel(); - if (model) { - this.telemetryService.publicLog2<{ source: string; action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: syncResource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); - const syncAreaLabel = getSyncAreaLabel(syncResource); - const result = await this.dialogService.confirm({ - type: 'info', - title: isRemote - ? localize('Sync accept remote', "{0}: {1}", SYNC_TITLE, acceptRemoteLabel) - : localize('Sync accept merges', "{0}: {1}", SYNC_TITLE, acceptMergesLabel), - message: isRemote - ? localize('confirm replace and overwrite local', "Would you like to accept remote {0} and replace local {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()) - : localize('confirm replace and overwrite remote', "Would you like to accept merges and replace remote {0}?", syncAreaLabel.toLowerCase()), - primaryButton: isRemote ? acceptRemoteButtonLabel : acceptMergesButtonLabel - }); - if (result.confirmed) { - try { - await this.userDataSyncService.accept(syncResource, model.uri, model.getValue(), true); - } catch (e) { - if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) { - const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(syncResourceCoflicts => syncResourceCoflicts[0] === syncResource)[0]; - if (syncResourceCoflicts && conflicts.some(conflict => isEqual(conflict.previewResource, model.uri) || isEqual(conflict.remoteResource, model.uri))) { - this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again.")); - } - } else { - this.notificationService.error(localize('accept failed', "Error while accepting changes. Please check [logs]({0}) for more details.", `command:${SHOW_SYNC_LOG_COMMAND_ID}`)); - } - } - } - } - })); - - this.acceptChangesButton.render(); - } - } - - private getSyncResourceConflicts(resource: URI): [SyncResource, IResourcePreview[]] | undefined { - return this.userDataSyncService.conflicts.filter(([, conflicts]) => conflicts.some(({ previewResource, remoteResource }) => isEqual(previewResource, resource) || isEqual(remoteResource, resource)))[0]; - } - - private disposeAcceptChangesWidgetRenderer(): void { - dispose(this.acceptChangesButton); - this.acceptChangesButton = undefined; - } - - override dispose(): void { - this.disposeAcceptChangesWidgetRenderer(); - super.dispose(); - } -} diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index 15a65e3ba18..210601cf0db 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -7,13 +7,13 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; import { isEqual } from 'vs/base/common/resources'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IViewsService } from 'vs/workbench/common/views'; import { VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browser/keybindingsEditorInput'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; @@ -22,7 +22,7 @@ export class UserDataSyncTrigger extends Disposable implements IWorkbenchContrib constructor( @IEditorService editorService: IEditorService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IViewsService viewsService: IViewsService, @IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService, @IHostService hostService: IHostService, @@ -56,10 +56,10 @@ export class UserDataSyncTrigger extends Disposable implements IWorkbenchContrib return 'keybindingsEditor'; } const resource = editorInput.resource; - if (isEqual(resource, this.environmentService.settingsResource)) { + if (isEqual(resource, this.userDataProfilesService.defaultProfile.settingsResource)) { return 'settingsEditor'; } - if (isEqual(resource, this.environmentService.keybindingsResource)) { + if (isEqual(resource, this.userDataProfilesService.defaultProfile.keybindingsResource)) { return 'keybindingsEditor'; } return undefined; diff --git a/src/vs/workbench/contrib/watermark/browser/watermark.ts b/src/vs/workbench/contrib/watermark/browser/watermark.ts index 799c6820bba..d20cf69cc08 100644 --- a/src/vs/workbench/contrib/watermark/browser/watermark.ts +++ b/src/vs/workbench/contrib/watermark/browser/watermark.ts @@ -181,7 +181,9 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr this.handleEditorPartSize(container, this.editorGroupsService.contentDimension); /* __GDPR__ - "watermark:open" : { } + "watermark:open" : { + "owner": "digitarald" + } */ this.telemetryService.publicLog('watermark:open'); } @@ -195,9 +197,7 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr this.watermark.remove(); const container = this.layoutService.getContainer(Parts.EDITOR_PART); - if (container) { - container.classList.remove('has-watermark'); - } + container?.classList.remove('has-watermark'); this.watermarkDisposable.clear(); } diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index 8c6cc98b2f4..d9cb718cefc 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -179,8 +179,8 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { this._container.style.height = `${dimension ? dimension.height : frameRect.height}px`; if (clippingContainer) { - const clip = computeClippingRect(frameRect, clippingContainer); - this._container.style.clip = `rect(${clip.top}px, ${clip.right}px, ${clip.bottom}px, ${clip.left}px)`; + const { top, left, right, bottom } = computeClippingRect(frameRect, clippingContainer); + this._container.style.clipPath = `polygon(${left}px ${top}px, ${right}px ${top}px, ${right}px ${bottom}px, ${left}px ${bottom}px)`; } } diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 625c46f3115..ec2b7281a65 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -12,6 +12,8 @@ </head> <body style="margin: 0; overflow: hidden; width: 100%; height: 100%" role="document"> + <!-- TODO: Remove additional script tag once Firefox is fixed https://bugzilla.mozilla.org/show_bug.cgi?id=1737882 --> + <script></script> <script async type="module"> // @ts-check /// <reference lib="dom" /> @@ -469,9 +471,14 @@ } if (body) { - body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast', 'vscode-reduce-motion', 'vscode-using-screen-reader'); + body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast', 'vscode-high-contrast-light', 'vscode-reduce-motion', 'vscode-using-screen-reader'); + if (initData.activeTheme) { body.classList.add(initData.activeTheme); + if (initData.activeTheme === 'vscode-high-contrast-light') { + // backwards compatibility + body.classList.add('vscode-high-contrast'); + } } if (initData.reduceMotion) { diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 457447af38e..326a076c677 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ <meta charset="UTF-8"> <meta http-equiv="Content-Security-Policy" - content="default-src 'none'; script-src 'sha256-xgIcbQmGjpT42GEj54VFSNh6MI15PZ2D1+DdVehfYBI=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> + content="default-src 'none'; script-src 'sha256-v9xEHcwDE5dc/lU7HYs5bG3LpPWGmQe0w/Vz6kmdd60=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> <!-- Disable pinch zooming --> <meta name="viewport" @@ -472,9 +472,14 @@ } if (body) { - body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast', 'vscode-reduce-motion', 'vscode-using-screen-reader'); + body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast', 'vscode-high-contrast-light', 'vscode-reduce-motion', 'vscode-using-screen-reader'); + if (initData.activeTheme) { body.classList.add(initData.activeTheme); + if (initData.activeTheme === 'vscode-high-contrast-light') { + // backwards compatibility + body.classList.add('vscode-high-contrast'); + } } if (initData.reduceMotion) { diff --git a/src/vs/workbench/contrib/webview/browser/themeing.ts b/src/vs/workbench/contrib/webview/browser/themeing.ts index 815dba525eb..c5a5d741a6b 100644 --- a/src/vs/workbench/contrib/webview/browser/themeing.ts +++ b/src/vs/workbench/contrib/webview/browser/themeing.ts @@ -90,7 +90,8 @@ export class WebviewThemeDataProvider extends Disposable { enum ApiThemeClassName { light = 'vscode-light', dark = 'vscode-dark', - highContrast = 'vscode-high-contrast' + highContrast = 'vscode-high-contrast', + highContrastLight = 'vscode-high-contrast-light', } namespace ApiThemeClassName { @@ -98,7 +99,8 @@ namespace ApiThemeClassName { switch (theme.type) { case ColorScheme.LIGHT: return ApiThemeClassName.light; case ColorScheme.DARK: return ApiThemeClassName.dark; - default: return ApiThemeClassName.highContrast; + case ColorScheme.HIGH_CONTRAST_DARK: return ApiThemeClassName.highContrast; + case ColorScheme.HIGH_CONTRAST_LIGHT: return ApiThemeClassName.highContrastLight; } } } diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts index 94ede834b02..9c8629d7e5f 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, DeferredPromise } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { memoize } from 'vs/base/common/decorators'; import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { EditorActivation } from 'vs/platform/editor/common/editor'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { GroupIdentifier } from 'vs/workbench/common/editor'; @@ -137,18 +137,43 @@ export class LazilyResolvedWebviewEditorInput extends WebviewInput { class RevivalPool { - private _awaitingRevival: Array<{ input: WebviewInput; resolve: () => void }> = []; + private _awaitingRevival: Array<{ + readonly input: WebviewInput; + readonly promise: DeferredPromise<void>; + readonly disposable: IDisposable; + }> = []; + + public enqueueForRestoration(input: WebviewInput, token: CancellationToken): Promise<void> { + const promise = new DeferredPromise<void>(); + + const remove = () => { + const index = this._awaitingRevival.findIndex(entry => input === entry.input); + if (index >= 0) { + this._awaitingRevival.splice(index, 1); + } + }; + + const disposable = combinedDisposable( + input.webview.onDidDispose(remove), + token.onCancellationRequested(() => { + remove(); + promise.cancel(); + }), + ); - public add(input: WebviewInput, resolve: () => void) { - this._awaitingRevival.push({ input, resolve }); + this._awaitingRevival.push({ input, promise, disposable }); + + return promise.p; } - public reviveFor(reviver: WebviewResolver, cancellation: CancellationToken) { + public reviveFor(reviver: WebviewResolver, token: CancellationToken) { const toRevive = this._awaitingRevival.filter(({ input }) => canRevive(reviver, input)); this._awaitingRevival = this._awaitingRevival.filter(({ input }) => !canRevive(reviver, input)); - for (const { input, resolve } of toRevive) { - reviver.resolveWebview(input, cancellation).then(resolve); + for (const { input, promise: resolve, disposable } of toRevive) { + reviver.resolveWebview(input, token).then(x => resolve.complete(x), err => resolve.error(err)).finally(() => { + disposable.dispose(); + }); } } } @@ -318,17 +343,12 @@ export class WebviewEditorService extends Disposable implements IWebviewWorkbenc return false; } - public resolveWebview( - webview: WebviewInput, - ): CancelablePromise<void> { + public resolveWebview(webview: WebviewInput): CancelablePromise<void> { return createCancelablePromise(async (cancellation) => { const didRevive = await this.tryRevive(webview, cancellation); if (!didRevive) { // A reviver may not be registered yet. Put into pool and resolve promise when we can revive - let resolve: () => void; - const promise = new Promise<void>(r => { resolve = r; }); - this._revivalPool.add(webview, resolve!); - return promise; + return this._revivalPool.enqueueForRestoration(webview, cancellation); } }); } diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 124df428f3f..37b194916ec 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -204,7 +204,7 @@ export class WebviewViewPane extends ViewPane { this.withProgress(async () => { await this.extensionService.activateByEvent(`onView:${this.id}`); - let self = this; + const self = this; const webviewView: WebviewView = { webview, onDidChangeVisibility: this.onDidChangeBodyVisibility, @@ -283,8 +283,8 @@ export class WebviewViewPane extends ViewPane { } if (this._rootContainer) { - const clip = computeClippingRect(this._container, this._rootContainer); - webviewEntry.container.style.clip = `rect(${clip.top}px, ${clip.right}px, ${clip.bottom}px, ${clip.left}px)`; + const { top, left, right, bottom } = computeClippingRect(this._container, this._rootContainer); + webviewEntry.container.style.clipPath = `polygon(${left}px ${top}px, ${right}px ${top}px, ${right}px ${bottom}px, ${left}px ${bottom}px)`; } } diff --git a/src/vs/workbench/contrib/welcomeBanner/browser/welcomeBanner.contribution.ts b/src/vs/workbench/contrib/welcomeBanner/browser/welcomeBanner.contribution.ts index f40532acab5..543ff8df1be 100644 --- a/src/vs/workbench/contrib/welcomeBanner/browser/welcomeBanner.contribution.ts +++ b/src/vs/workbench/contrib/welcomeBanner/browser/welcomeBanner.contribution.ts @@ -26,7 +26,7 @@ class WelcomeBannerContribution { return; // welcome banner is not enabled } - if (storageService.getBoolean(WelcomeBannerContribution.WELCOME_BANNER_DISMISSED_KEY, StorageScope.GLOBAL, false)) { + if (storageService.getBoolean(WelcomeBannerContribution.WELCOME_BANNER_DISMISSED_KEY, StorageScope.PROFILE, false)) { return; // welcome banner dismissed } @@ -43,7 +43,7 @@ class WelcomeBannerContribution { icon, actions: welcomeBanner.actions, onClose: () => { - storageService.store(WelcomeBannerContribution.WELCOME_BANNER_DISMISSED_KEY, true, StorageScope.GLOBAL, StorageTarget.MACHINE); + storageService.store(WelcomeBannerContribution.WELCOME_BANNER_DISMISSED_KEY, true, StorageScope.PROFILE, StorageTarget.MACHINE); } }); } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index 7e276d7a2a7..f18744b2c6c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -30,6 +30,7 @@ import { isLinux, isMacintosh, isWindows, OperatingSystem as OS } from 'vs/base/ import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { StartupPageContribution, } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; +import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; export * as icons from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedIcons'; @@ -49,10 +50,15 @@ registerAction2(class extends Action2 { }); } - public run(accessor: ServicesAccessor, walkthroughID: string | { category: string; step: string } | undefined, toSide: boolean | undefined) { + public run( + accessor: ServicesAccessor, + walkthroughID: string | { category: string; step: string } | undefined, + toSide: boolean | undefined + ) { const editorGroupsService = accessor.get(IEditorGroupsService); const instantiationService = accessor.get(IInstantiationService); const editorService = accessor.get(IEditorService); + const commandService = accessor.get(ICommandService); if (walkthroughID) { const selectedCategory = typeof walkthroughID === 'string' ? walkthroughID : walkthroughID.category; @@ -80,8 +86,25 @@ registerAction2(class extends Action2 { } } - // Otherwise, just make a new one. - editorService.openEditor(instantiationService.createInstance(GettingStartedInput, { selectedCategory: selectedCategory, selectedStep: selectedStep }), {}, toSide ? SIDE_GROUP : undefined); + const activeEditor = editorService.activeEditor; + // If the walkthrough is already open just reveal the step + if (selectedStep && activeEditor instanceof GettingStartedInput && activeEditor.selectedCategory === selectedCategory) { + commandService.executeCommand('walkthroughs.selectStep', selectedStep); + return; + } + + const gettingStartedInput = instantiationService.createInstance(GettingStartedInput, { selectedCategory: selectedCategory, selectedStep: selectedStep }); + // If it's the extension install page then lets replace it with the getting started page + if (activeEditor instanceof ExtensionsInput) { + const activeGroup = editorGroupsService.activeGroup; + activeGroup.replaceEditors([{ + editor: activeEditor, + replacement: gettingStartedInput + }]); + } else { + // else open respecting toSide + editorService.openEditor(gettingStartedInput, { preserveFocus: toSide ?? false }, toSide ? SIDE_GROUP : undefined); + } } else { editorService.openEditor(new GettingStartedInput({}), {}); } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 5fac78668bf..f119f79c69d 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -469,14 +469,14 @@ export class GettingStartedPage extends EditorPane { } private getHiddenCategories(): Set<string> { - return new Set(JSON.parse(this.storageService.get(hiddenEntriesConfigurationKey, StorageScope.GLOBAL, '[]'))); + return new Set(JSON.parse(this.storageService.get(hiddenEntriesConfigurationKey, StorageScope.PROFILE, '[]'))); } private setHiddenCategories(hidden: string[]) { this.storageService.store( hiddenEntriesConfigurationKey, JSON.stringify(hidden), - StorageScope.GLOBAL, + StorageScope.PROFILE, StorageTarget.USER); } @@ -643,8 +643,13 @@ export class GettingStartedPage extends EditorPane { } async selectStepLoose(id: string) { - const toSelect = this.editorInput.selectedCategory + '#' + id; - this.selectStep(toSelect); + // Allow passing in id with a category appended or with just the id of the step + if (id.startsWith(`${this.editorInput.selectedCategory}#`)) { + this.selectStep(id); + } else { + const toSelect = this.editorInput.selectedCategory + '#' + id; + this.selectStep(toSelect); + } } private async selectStep(id: string | undefined, delayFocus = true, forceRebuild = false) { @@ -795,9 +800,9 @@ export class GettingStartedPage extends EditorPane { await this.gettingStartedService.installedExtensionsRegistered; this.container.classList.remove('loading'); this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); + this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); } - this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); if (!this.currentWalkthrough) { console.error('Could not restore to category ' + this.editorInput.selectedCategory + ' as it was not found'); this.editorInput.selectedCategory = undefined; @@ -815,7 +820,7 @@ export class GettingStartedPage extends EditorPane { this.buildTelemetryFooter(telemetryNotice); footer.appendChild(telemetryNotice); } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory) { - const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL) || new Date().toUTCString(); + const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION) || new Date().toUTCString(); const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; @@ -1152,7 +1157,7 @@ export class GettingStartedPage extends EditorPane { this.storageService.store( restoreWalkthroughsConfigurationKey, JSON.stringify(restoreData), - StorageScope.GLOBAL, StorageTarget.MACHINE); + StorageScope.PROFILE, StorageTarget.MACHINE); this.hostService.openWindow([{ folderUri: toOpen }]); } }); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts index 6fef8b0769f..50144d87e1c 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedList.ts @@ -115,9 +115,7 @@ export class GettingStartedIndexList<T extends { id: string; when?: ContextKeyEx this.contextKeysToWatch.clear(); entryList.forEach(e => { const keys = e.when?.keys(); - if (keys) { - keys.forEach(key => this.contextKeysToWatch.add(key)); - } + keys?.forEach(key => this.contextKeysToWatch.add(key)); }); this.lastRendered = toRender; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 4be26d8aedc..74b541681a6 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -136,7 +136,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ private steps = new Map<string, IWalkthroughStep>(); private tasExperimentService?: IWorkbenchAssignmentService; - private sessionInstalledExtensions = new Set<string>(); + private sessionInstalledExtensions: Set<string> = new Set<string>(); private categoryVisibilityContextKeys = new Set<string>(); private stepCompletionContextKeyExpressions = new Set<ContextKeyExpression>(); @@ -167,10 +167,10 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this.metadata = new Map( JSON.parse( - this.storageService.get(walkthroughMetadataConfigurationKey, StorageScope.GLOBAL, '[]'))); + this.storageService.get(walkthroughMetadataConfigurationKey, StorageScope.PROFILE, '[]'))); this.memento = new Memento('gettingStartedService', this.storageService); - this.stepProgress = this.memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + this.stepProgress = this.memento.getMemento(StorageScope.PROFILE, StorageTarget.USER); walkthroughsExtensionPoint.setHandler(async (_, { added, removed }) => { await Promise.all( @@ -235,7 +235,9 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this._register(this.extensionManagementService.onDidInstallExtensions(async (result) => { const hadLastFoucs = await this.hostService.hadLastFocus(); for (const e of result) { - if (hadLastFoucs) { + // If the window had last focus and the install didn't specify to skip the walkthrough + // Then add it to the sessionInstallExtensions to be opened + if (hadLastFoucs && !e?.context?.skipWalkthrough) { this.sessionInstalledExtensions.add(e.identifier.id.toLowerCase()); } this.progressByEvent(`extensionInstalled:${e.identifier.id.toLowerCase()}`); @@ -273,7 +275,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this.metadata.set(id, { ...prior, manaullyOpened: true, stepIDs: walkthrough.steps.map(s => s.id) }); } - this.storageService.store(walkthroughMetadataConfigurationKey, JSON.stringify([...this.metadata.entries()]), StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(walkthroughMetadataConfigurationKey, JSON.stringify([...this.metadata.entries()]), StorageScope.PROFILE, StorageTarget.USER); } private async registerExtensionWalkthroughContributions(extension: IExtensionDescription) { @@ -423,14 +425,13 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this._onDidAddWalkthrough.fire(this.resolveWalkthrough(walkthoughDescriptor)); })); - this.storageService.store(walkthroughMetadataConfigurationKey, JSON.stringify([...this.metadata.entries()]), StorageScope.GLOBAL, StorageTarget.USER); - + this.storageService.store(walkthroughMetadataConfigurationKey, JSON.stringify([...this.metadata.entries()]), StorageScope.PROFILE, StorageTarget.USER); if (sectionToOpen && this.configurationService.getValue<string>('workbench.welcomePage.walkthroughs.openOnInstall')) { type GettingStartedAutoOpenClassification = { id: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; - owner: 'JacksonKearl'; + owner: 'lramos15'; comment: 'Used to understand what walkthroughs are consulted most frequently'; }; }; @@ -438,7 +439,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ id: string; }; this.telemetryService.publicLog2<GettingStartedAutoOpenEvent, GettingStartedAutoOpenClassification>('gettingStarted.didAutoOpenWalkthrough', { id: sectionToOpen }); - this.commandService.executeCommand('workbench.action.openWalkthrough', sectionToOpen); + this.commandService.executeCommand('workbench.action.openWalkthrough', sectionToOpen, true); } } @@ -703,17 +704,17 @@ registerAction2(class extends Action2 { storageService.store( hiddenEntriesConfigurationKey, JSON.stringify([]), - StorageScope.GLOBAL, + StorageScope.PROFILE, StorageTarget.USER); storageService.store( walkthroughMetadataConfigurationKey, JSON.stringify([]), - StorageScope.GLOBAL, + StorageScope.PROFILE, StorageTarget.USER); const memento = new Memento('gettingStartedService', accessor.get(IStorageService)); - const record = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + const record = memento.getMemento(StorageScope.PROFILE, StorageTarget.USER); for (const key in record) { if (Object.prototype.hasOwnProperty.call(record, key)) { try { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts index e7a2c51207e..d5cfa8c3323 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts @@ -59,9 +59,9 @@ export class StartupPageContribution implements IWorkbenchContribution { && this.productService.showTelemetryOptOut && getTelemetryLevel(this.configurationService) !== TelemetryLevel.NONE && !this.environmentService.skipWelcome - && !this.storageService.get(telemetryOptOutStorageKey, StorageScope.GLOBAL) + && !this.storageService.get(telemetryOptOutStorageKey, StorageScope.PROFILE) ) { - this.storageService.store(telemetryOptOutStorageKey, true, StorageScope.GLOBAL, StorageTarget.USER); + this.storageService.store(telemetryOptOutStorageKey, true, StorageScope.PROFILE, StorageTarget.USER); await this.openGettingStarted(true); return; } @@ -94,7 +94,7 @@ export class StartupPageContribution implements IWorkbenchContribution { } private tryOpenWalkthroughForFolder(): boolean { - const toRestore = this.storageService.get(restoreWalkthroughsConfigurationKey, StorageScope.GLOBAL); + const toRestore = this.storageService.get(restoreWalkthroughsConfigurationKey, StorageScope.PROFILE); if (!toRestore) { return false; } @@ -107,7 +107,7 @@ export class StartupPageContribution implements IWorkbenchContribution { GettingStartedInput, { selectedCategory: restoreData.category, selectedStep: restoreData.step }), { pinned: false }); - this.storageService.remove(restoreWalkthroughsConfigurationKey, StorageScope.GLOBAL); + this.storageService.remove(restoreWalkthroughsConfigurationKey, StorageScope.PROFILE); return true; } } diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts index fc8d43ad75b..01504f62f36 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -157,7 +157,7 @@ export class WalkThroughPart extends EditorPane { this.content.addEventListener('click', event => { for (let node = event.target as HTMLElement; node; node = node.parentNode as HTMLElement) { if (node instanceof HTMLAnchorElement && node.href) { - let baseElement = window.document.getElementsByTagName('base')[0] || window.location; + const baseElement = window.document.getElementsByTagName('base')[0] || window.location; if (baseElement && node.href.indexOf(baseElement.href) >= 0 && node.hash) { const scrollTarget = this.content.querySelector(node.hash); const innerContent = this.content.firstElementChild; @@ -291,9 +291,7 @@ export class WalkThroughPart extends EditorPane { this.updateSizeClasses(); this.decorateContent(); this.contentDisposables.push(this.keybindingService.onDidUpdateKeybindings(() => this.decorateContent())); - if (input.onReady) { - input.onReady(this.content.firstElementChild as HTMLElement, store); - } + input.onReady?.(this.content.firstElementChild as HTMLElement, store); this.scrollbar.scanDomNode(); this.loadTextEditorViewState(input); this.updatedScrollPosition(); @@ -371,9 +369,7 @@ export class WalkThroughPart extends EditorPane { this.multiCursorModifier(); } })); - if (input.onReady) { - input.onReady(innerContent, store); - } + input.onReady?.(innerContent, store); this.scrollbar.scanDomNode(); this.loadTextEditorViewState(input); this.updatedScrollPosition(); diff --git a/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css index 570b6006df0..827ea1c2c4c 100644 --- a/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css @@ -7,7 +7,8 @@ font-family: 'codicon'; content: '\eb53'; background-image: none; - font-size: 150%; + font-size: 16px; + line-height: 37px !important; } .workspace-trust-editor { |